Repository navigation
fix: reject NUL bytes and invalid UTF-8 in text parameters with a 400 - #1143
Merged
alexluong merged 1 commit intoOct 13, 2026
Merged
Conversation
PostgreSQL refuses a NUL byte or invalid UTF-8 in a text parameter (SQLSTATE 22021), so GET /api/v1/events?tenant_id=%00 and similar requests failed with a 500 on the PostgreSQL log store. TextParamsMiddleware runs after auth on every API route and answers a path or query parameter holding such a value with a 400 naming it. A cursor whose decoded position holds one is now an invalid cursor, so a hand-built cursor gets the same 400 on every log store instead of a 500 on PostgreSQL and a 200 on ClickHouse. Fixes hookdeck#1114
alexluong
approved these changes
Oct 13, 2026
alexluong
left a comment
Collaborator
There was a problem hiding this comment.
Thanks for your first contribution to Outpost, and welcome! This looks good to me, merging it in.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1114
A NUL byte or invalid UTF-8 in a path or query parameter, or in the position of a hand-built cursor, now gets a 400 on every log store instead of a 500 from PostgreSQL (SQLSTATE 22021).
Change
TextParamsMiddleware(internal/apirouter/textparams_middleware.go) runs after auth on every API route. A path or query value holding a NUL byte or invalid UTF-8 is answered with a 400 that names the parameter, in the same style as the limit errors:invalid tenant_id: must be valid UTF-8 without NUL bytes. It covers the list filters, thetenant_idquery on the retrieve endpoints, the metrics filters and path IDs like/events/:event_id, so nothing like that reaches a store. Valid non-ASCII text is untouched.cursor.Decodetreats a position with a NUL byte or invalid UTF-8 asinvalid cursor. The cursor itself is base62, so it gets through the middleware; the check has to happen after decoding. This gives the same 400 on PostgreSQL (500 before) and ClickHouse (200 before).I put the check in the router rather than in
pglogstoreso all log stores answer the same way, which is what the issue asks for. It also covers path parameters that go to the tenant store.Tests
TestAPI_InvalidTextParamReturns400: NUL and invalid UTF-8 in query values, in one value of an array parameter, in path parameters (including nested ones) and in a metrics filter.TestAPI_ValidTextParamIsAccepted: plain and non-ASCII values still list normally.TestAPI_InvalidCursorReturns400: two new cases, a position with a NUL byte and one with invalid UTF-8, on all four lists.TestDecode: the same at thecursorpackage level.On the previous code the new API tests fail (200 or 404 on the mem store, where PostgreSQL gave the 500), and the tenant cursor cases give
invalid cursor: invalid timestamp. With the change,go test -short ./internal/apirouter/ ./internal/cursor/passes, along withgo build ./...,go vetandgofmt. I didn't run the PostgreSQL integration suite locally; the change sits in front of the store, so the store code is unchanged.