fix: return 400 instead of 500 for malformed pageToken on list endpoints - #5466
fix: return 400 instead of 500 for malformed pageToken on list endpoints#5466youngrae81 wants to merge 3 commits into
Conversation
| try { | ||
| var bytes = Base64.getUrlDecoder().decode(requestedPageToken); | ||
| return SMILE_MAPPER.readValue(bytes, PageToken.class); | ||
| } catch (IllegalArgumentException | IllegalStateException | JacksonException e) { |
There was a problem hiding this comment.
I believe this catch doesn't get every exception a corrupted SMILE payload can throw.
For instance, fuzzing a valid token throws ArrayIndexOutOfBoundsException, which isn't caught here and falls through to IcebergExceptionMapper's default 500.
I suggest widening to catch (RuntimeException e) instead of enumerating types.
There was a problem hiding this comment.
Good catch, thanks. Fuzzing a valid token reproduced it: 4 of 60k random corruptions escaped as ArrayIndexOutOfBoundsException. Widened to RuntimeException in 04ed32a and added the fuzz-found token as a fixed regression case.
| * failure is client input that cannot be interpreted and must surface as an {@link | ||
| * IllegalArgumentException} (mapped to HTTP 400), never as a server error. | ||
| */ | ||
| private static PageToken deserializePageToken(String requestedPageToken) { |
There was a problem hiding this comment.
Maybe we could reuse the Polaris standard serialization with Jackson 3?
There was a problem hiding this comment.
I looked at PolarisObjectMapperUtil, but it is a Jackson 2 JSON mapper, while page tokens are binary SMILE via Jackson 3 (the SmileMapper here predates this PR).
I kept it as is; happy to switch if there is a shared Jackson 3 mapper you had in mind.
|
|
||
| private PageTokenUtil() {} | ||
|
|
||
| @VisibleForTesting |
There was a problem hiding this comment.
Doesn't it overlap with the existing serializePageToken()?
There was a problem hiding this comment.
Agreed, removed in 04ed32a. The test now builds its own plain SmileMapper to craft non-PageToken payloads, so the production change is just the catch block.
| arguments("valid base64, not SMILE", "AAAA"), | ||
| arguments( | ||
| "valid base64, plain text", | ||
| encode("hello world".getBytes(java.nio.charset.StandardCharsets.UTF_8))), |
There was a problem hiding this comment.
Maybe we should cover the byte-level/bit-flip corruption?
There was a problem hiding this comment.
Added in 04ed32a: a full single-bit-flip and 0x00/0xFF byte-overwrite sweep over a valid token, plus a seeded random corruption test (overwrite/truncate/insert/flip, 20k iterations) over tokens of different shapes.
A corrupted token may still decode, but must never fail with anything other than IllegalArgumentException.
4a9ffbd to
7beee54
Compare
`PageTokenUtil.decodePageRequest` decoded the client-supplied `pageToken`
(Base64 + SMILE) without error handling. A token that is valid Base64 but
not a serialized page token (garbage, truncated, or produced by an
incompatible Polaris version) escaped as a Jackson 3 `JacksonException`
or, for an unknown token type id, as `IllegalStateException`. Neither is
covered by the REST exception mappers, so client input turned into a
500 with an ERROR-level stack trace.
Wrap the decoding step and rethrow any failure as
`IllegalArgumentException("Invalid page token")`, which the existing
`IcebergExceptionMapper` maps to 400. The original exception is kept as
the cause for debug logging.
Adds a parameterized unit test covering the malformed-token shapes and
an integration test asserting the HTTP 400 on `listNamespaces`.
Fixes apache#5465
…upted tokens - Widen the catch in `deserializePageToken` to `RuntimeException`: fuzzing a valid token shows the SMILE parser can fail with `ArrayIndexOutOfBoundsException`, which the enumerated catch let through to the default 500 mapping. - Remove the `smileMapperForTests()` accessor; the test builds its own plain `SmileMapper` to craft payloads that are not page tokens. - Add the fuzz-found token as a fixed regression case, a bit-flip / byte-overwrite sweep over a valid token, and a seeded random corruption test (overwrite, truncate, insert, flip) across tokens of different shapes. A corrupted token may still decode, but must never fail with anything other than `IllegalArgumentException`.
7beee54 to
04ed32a
Compare
flyingImer
left a comment
There was a problem hiding this comment.
I think one remaining case needs fixing before merge: malformed page-token input can still return 500. Details inline.
| private static PageToken deserializePageToken(String requestedPageToken) { | ||
| try { | ||
| var bytes = Base64.getUrlDecoder().decode(requestedPageToken); | ||
| return SMILE_MAPPER.readValue(bytes, PageToken.class); |
There was a problem hiding this comment.
Could we also reject a null result here? A SMILE root null deserializes successfully, so this catch never runs IIUC. With pageSize supplied, decodePageRequest then dereferences that null outside the catch and the request still returns 500. Please reject it as an invalid page token and add a regression case confirming 400 both with and without pageSize.
There was a problem hiding this comment.
Good catch, thank you.
Reproduced: without pageSize the null token was returned to the caller, and with pageSize it failed with a NullPointerException outside the catch. The latest commit rejects a null result as an invalid page token, and the test class now has a "SMILE-encoded null" case plus a parameterized test covering pageSize absent, 0 and 5.
A SMILE-encoded null is valid input for the mapper and deserializes to a null PageToken, so the catch around decoding never runs. Without a page size the null was returned to the caller; with a page size, decodePageRequest dereferenced it outside the catch and the request still ended as a 500. Reject a null result as an invalid page token and add regression cases with and without a page size.
PageTokenUtil.decodePageRequestdecoded the client-suppliedpageToken(Base64 + SMILE) without error handling. A token that is valid Base64 but not a serialized page token (garbage, truncated, or produced by an incompatible Polaris version) escaped as a Jackson 3JacksonExceptionor, for an unknown token type id, asIllegalStateException. Neither is covered by the REST exception mappers, so client input turned into a 500 with an ERROR-level stack trace.Wrap the decoding step and rethrow any failure as
IllegalArgumentException("Invalid page token"), which the existingIcebergExceptionMappermaps to 400. The original exception is kept as the cause for debug logging.Adds a parameterized unit test covering the malformed-token shapes and an integration test asserting the HTTP 400 on
listNamespaces.Fixes #5465
Checklist
CHANGELOG.md(if needed)site/content/in-dev/unreleased(if needed)