GetLogEvents publishes a pair of directional pagination tokens and substrate implements half of
one of them, so a caller that follows the documented termination rule cannot terminate.
What the page publishes
API_GetLogEvents publishes two response elements, both Length Constraints: Minimum length of 1:
nextBackwardToken — "The token for the next set of items in the backward direction. The token
expires after 24 hours. This token is not null. If you have reached the end of the stream, it
returns the same token you passed in."
nextForwardToken — "The token for the next set of items in the forward direction. The token
expires after 24 hours. If you have reached the end of the stream, it returns the same token you
passed in."
And, in the operation's own overview:
GetLogEvents is a paginated operation. … As long as the nextBackwardToken or nextForwardToken
returned is NOT equal to the nextToken that you passed into the API call, there might be more log
events available. The token that you use depends on the direction you want to move in along the log
stream. The returned tokens are never null.
The request parameter startFromHead also publishes a rule that depends on the pair: "If you are
using a previous nextForwardToken value as the nextToken in this operation, you must specify
true for startFromHead."
What substrate does
getLogEvents (emulator/cloudwatchlogs_plugin.go) declares both members omitempty, never sets
nextBackwardToken at all, and sets nextForwardToken only when a further page exists. So:
nextBackwardToken is always absent. A caller reading the member AWS says "is not null"
reads nothing.
- At the end of the stream both are absent, where AWS returns the same token you passed in.
- Therefore the published termination rule is unusable against substrate: a loop written as
"stop when the returned token equals the one I sent" never sees a returned token, so it either
compares against an empty string on the first call and stops immediately, or spins. A caller has
to use the empty-token rule instead, which is what the other three Logs paginators publish and
this one does not.
startFromHead is not read at all, so the direction the pair exists to express is unmodelled.
This is not a token-validity defect. #1086 converted all four Logs paginators to refuse a
nextToken substrate could not have issued, and recorded this as explicitly out of its scope so the
conversion would not be read as having fixed it — see emulator/cloudwatchlogs_pagination.go and the
docs/services.md section "CloudWatch Logs carried one block four times".
The second half: the published 24-hour expiry
All four Logs paginators publish "The token expires after 24 hours." on the response member, and
no page publishes a code for presenting an expired token — the same shape as EventBridge's
InvalidToken finding with the conclusion missing. Substrate's tokens never expire. Modelling it is
squarely in scope per CLAUDE.md (the simulated clock, not wall time, would judge it) but needs a
code decision #1086 deliberately did not take: the only code all four pages publish for a bad
parameter is InvalidParameterException/400, and attributing expiry to it is a reading, not an
attribution.
Scope this issue to the token pair; the expiry can ship with it or separately, but the decision
should be recorded either way.
Acceptance criteria
GetLogEventspublishes a pair of directional pagination tokens and substrate implements half ofone of them, so a caller that follows the documented termination rule cannot terminate.
What the page publishes
API_GetLogEventspublishes two response elements, bothLength Constraints: Minimum length of 1:nextBackwardToken— "The token for the next set of items in the backward direction. The tokenexpires after 24 hours. This token is not null. If you have reached the end of the stream, it
returns the same token you passed in."
nextForwardToken— "The token for the next set of items in the forward direction. The tokenexpires after 24 hours. If you have reached the end of the stream, it returns the same token you
passed in."
And, in the operation's own overview:
The request parameter
startFromHeadalso publishes a rule that depends on the pair: "If you areusing a previous
nextForwardTokenvalue as thenextTokenin this operation, you must specifytrueforstartFromHead."What substrate does
getLogEvents(emulator/cloudwatchlogs_plugin.go) declares both membersomitempty, never setsnextBackwardTokenat all, and setsnextForwardTokenonly when a further page exists. So:nextBackwardTokenis always absent. A caller reading the member AWS says "is not null"reads nothing.
"stop when the returned token equals the one I sent" never sees a returned token, so it either
compares against an empty string on the first call and stops immediately, or spins. A caller has
to use the empty-token rule instead, which is what the other three Logs paginators publish and
this one does not.
startFromHeadis not read at all, so the direction the pair exists to express is unmodelled.This is not a token-validity defect. #1086 converted all four Logs paginators to refuse a
nextTokensubstrate could not have issued, and recorded this as explicitly out of its scope so theconversion would not be read as having fixed it — see
emulator/cloudwatchlogs_pagination.goand thedocs/services.mdsection "CloudWatch Logs carried one block four times".The second half: the published 24-hour expiry
All four Logs paginators publish "The token expires after 24 hours." on the response member, and
no page publishes a code for presenting an expired token — the same shape as EventBridge's
InvalidTokenfinding with the conclusion missing. Substrate's tokens never expire. Modelling it issquarely in scope per
CLAUDE.md(the simulated clock, not wall time, would judge it) but needs acode decision #1086 deliberately did not take: the only code all four pages publish for a bad
parameter is
InvalidParameterException/400, and attributing expiry to it is a reading, not anattribution.
Scope this issue to the token pair; the expiry can ship with it or separately, but the decision
should be recorded either way.
Acceptance criteria
nextForwardTokenis present on every successfulGetLogEventsresponse, including the lastpage, and at the end of the stream equals the
nextTokenthe caller passed in (or thefirst-page token when none was passed).
nextBackwardTokenis present on every successful response and is never empty.forward offset (and vice versa) — the Fifteen offset-token sites still answer page one for a token substrate never issued #1086 refusal must keep working, and a directional token
that decodes cleanly in the wrong direction is the same class of silent wrong answer.
startFromHeadis read, or its absence is recorded in the plugin as a deliberate divergencewith the reason.
returned token equalling the one passed in — holds, driven entirely through the wire.
clock with the code choice argued, or declined in
docs/services.mdwith the reason.docs/services.md's CloudWatch Logs section stops listing the missing backward token among thedivergences Fifteen offset-token sites still answer page one for a token substrate never issued #1086 left in place.