Skip to content

feat(ktor): resolve @HttpHeader controller parameters - #64

Merged
Ziedelth merged 3 commits into
masterfrom
feat/http-header-parameter
Oct 26, 2026
Merged

Ziedelth merged 3 commits into
masterfrom
feat/http-header-parameter

Conversation

@Ziedelth

Copy link
Copy Markdown
Contributor

A controller had no way to read a request header. A conditional request (If-None-Match, If-Modified-Since), an idempotency key or a correlation identifier could not reach one without dropping to a raw Ktor route — which the controller conventions forbid.

What it adds

  • @HttpHeader(name, required = true) on a controller parameter, resolved from call.request.headers with the same conversions as @QueryParam (String, Int, Long, Double, Boolean, enums, Uuid).
  • An absent optional header binds null as before; an absent required header is refused with InvalidParameterException, which the binder already turns into HTTP 400 instead of an unresolved parameter.
  • OpenApiOperationDescriber emits the parameter as a header, so the generated contract stays exact.

Why it matters downstream

Shikkanime/core serves artwork under an immutable, versioned URL. Without header access it cannot answer 304 Not Modified to a client that sends its ETag back; with this annotation the controller branch is writable, and the response stays inside the controller pipeline.

Checks

  • ./gradlew :ktor:test — 47 tests, 0 failure, including three new resolver cases (present, absent optional, absent required).
  • The framework publishes 0.0.25-SNAPSHOT on merge, so consumers pick the annotation up without a version bump.

A controller had no way to read a request header: a conditional request (`If-None-Match`,
`If-Modified-Since`), an idempotency key or a correlation identifier could not reach one without
dropping to a raw Ktor route, which the controller conventions forbid.

`@HttpHeader(name, required)` resolves a parameter from the request headers, with the same
conversions as `@QueryParam`. An absent optional header binds `null`; an absent required header is
refused with `InvalidParameterException`, which the binder already answers as HTTP 400. The OpenAPI
description emits the parameter as a header, so the generated contract stays exact.

Covered by three resolver cases: present, absent optional, absent required.
A controller returning raw bytes had to build its own `OutgoingContent` to keep its media type and
its cache headers, which every consumer project reimplements.

`ResponseEntity.binary(payload, contentType, etag, cacheControl)`, `ResponseEntity.redirect(location)`
and `ResponseEntity.notModified(etag)` wrap the new `BinaryContent`, `LocationContent` and
`NotModifiedContent` bodies: the binder answers with them as-is, so the content type and the headers
reach the client without leaving the controller pipeline.

Covered by three factory cases: payload with its cache metadata, location, revalidated validator.
The annotation and the response factories land together: merging this branch now publishes a version that carries them, instead of overwriting a snapshot other builds already resolved.

Tests also use `assertThrows` through an import rather than its qualified name.
@Ziedelth
Ziedelth merged commit 486f9bd into master Oct 26, 2026
2 checks passed
@Ziedelth
Ziedelth deleted the feat/http-header-parameter branch October 26, 2026 08:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant