Repository navigation
Bind controller arguments from request params; parse urlencoded form bodies - #54
Merged
Merged
Conversation
Annotated route methods can now take arguments instead of calling param(): each is looked up by @PARAM name (or the Java parameter name when compiled with -parameters) from path then query parameters, and converted to String, primitives/wrappers, enums or UUID. Context and InputStream arguments get the request context and body. Unconvertible values, and missing values for primitives, answer 400. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Leqha458EwWbRT5xWiVuyg
Fields of an application/x-www-form-urlencoded body are read lazily with form()/forms(), and param() falls back to them after path and query parameters, so controller method arguments bind form fields too. The body is decoded with the request charset (UTF-8 by default) and stays readable through body(). Bodies over App.setMaxFormSize (2 MB default) get 413, malformed ones 400, via a RequestException that App.handle turns into a response; controller argument errors use it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Leqha458EwWbRT5xWiVuyg
- A 400/413 raised mid-handler (bad form body, unconvertible controller argument) now replaces the partial response instead of being appended to it. Context gains a default reset() hook that HttpContext implements; App.handle calls it before writing the error. - context().body() now goes through the same form-aware body as Turismo.body(), so both replay the body after the form was parsed. - Reading form fields after the handler took the raw body stream now throws IllegalStateException with a clear message, instead of quietly returning no fields. - Rename forms() to formFields() (unreleased API). - @PARAM javadoc: lookup includes form fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Leqha458EwWbRT5xWiVuyg
When a controller class is compiled without -parameters, read the parameter names from the class file's LocalVariableTable instead. That table is written with debug information (-g), the default for Maven, Gradle and IDE builds, so plain `void item(int id)` binds "id" without @PARAM in the common case. Only classes compiled with neither -g nor -parameters still need @PARAM, and the registration error now says so. ParameterNames is a small, dependency-free class-file reader that only walks far enough to reach the method's Code/LocalVariableTable; any surprise yields null and the existing error path. Tests compile a controller at runtime with -g, -g:none and -parameters to cover each case, including long/double slot widths and static methods. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Leqha458EwWbRT5xWiVuyg
Controller arguments: - BigInteger and BigDecimal, capped at 1000 characters and (for BigDecimal) a scale magnitude of 1000; beyond that parsing or printing grows far faster than the input, so a short request like d=1e999999999 could otherwise tie up the server. Over the cap is 400. - Arrays of any supported type (String[], int[], Boolean[], BigDecimal[], enum arrays, ...) collect every value of a repeated parameter in order; an absent parameter gives an empty array, a bad element 400. Nested or unsupported arrays fail at registration. Repeated parameters: - Context.queryValues(name), with a default so custom contexts still compile; HttpContext keeps all values and query() now returns the first (was the last), matching form fields and servlet getParameter. - Form keeps all values of a repeated field. - New Turismo.paramValues/queryValues/formValues. paramValues looks in the same places as param() and returns all values from the first place that has the name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Leqha458EwWbRT5xWiVuyg
A List<T>, Collection<T>, Iterable<T> or Set<T> argument, where T is any type an array argument accepts, collects every value of a repeated parameter. The element type comes from the declared type argument (List<? extends X> uses X). Set gives a LinkedHashSet, so request order is kept and duplicates dropped; the others an ArrayList. Each request gets a fresh, mutable collection, empty if the parameter is absent. A bad element is 400. A raw type, List<?>, List<Object>, a lower-bounded wildcard, a type variable or a nested collection has no usable element type and is rejected at registration, as are concrete collection classes. testControllerRejectsUnsupportedParameterType used List<String> as its unsupported example; it now uses Map<String, String>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Leqha458EwWbRT5xWiVuyg
ghosthack
force-pushed
the
claude/vigilant-allen-k2k12x
branch
from
September 27, 2026 13:31
ec6075f to
8f72170
Compare
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.
Follow-up to #53, for 5.0.0 (not yet tagged).
Summary
Controller argument binding (
d90b4d6,0213ee8,dec0f0c,8f72170)@Paramis optional:param()(path, then query, then form fields).String, primitives and their wrappers (boolean/Booleanaccepttrue/falsein any case),BigInteger,BigDecimal, enums (by constant name) andUUID.Contextargument receives the request context and anInputStreamargument the request body.List,Collection,IterableorSetof one (List<? extends X>usesX).Setis aLinkedHashSet(request order, duplicates dropped); the others are anArrayList. Each request gets a fresh, mutable collection.List,List<?>,List<Object>,List<? super X>, type variables, nested collections), as well as concrete collection classes.BigDecimalexponent at ±1000, because parsing or printing far larger values grows much faster than the input. A 20-characterd=1e999999999would otherwise print as a billion digits. Over the cap gets400.400 Bad Request. A missing value for any other type is passed asnull.@Param, the Java parameter name is read from the class file:-parametersmetadata when the class was compiled with it;LocalVariableTablewritten by debug info (-g), the default in Maven, Gradle and IDE builds.@Param;controller()rejects them at registration with a message saying so.ParameterNamesis a small, dependency-free class-file reader, and anything unexpected falls back to that error.Repeated parameters (
dec0f0c)Turismo.paramValues(name),queryValues(name)andformValues(name)return every value of a repeated name.paramValueslooks in the same places asparam().ContextgainsqueryValues(name), with a default that returnsquery(name)so custom contexts keep compiling.param()/query()now return the first value of a repeated query parameter. 4.x returned the last. This matches form fields and servletgetParameter, and is listed in the README's "Upgrading to 5.0".Form bodies (
3e66afc)application/x-www-form-urlencodedbodies are parsed on first use:form(name),formFields()andformValues(name)read the fields.param(name)falls back to form fields after path and query parameters.Content-Typeand defaults to UTF-8.body()andcontext().body()replay the raw bytes.App.setMaxFormSize(bytes)sets the size limit (default 2 MB). Oversized bodies get413; a malformed body gets400.Review fixes (
0940d51)Context.reset()).context().body()now goes through the same form-aware body asTurismo.body().IllegalStateExceptioninstead of returning no fields.forms()is nowformFields().Testing
mvn verifypasses, with 243 tests, and javadoc builds.masteris green.ArgumentTypesTestcovers:Setordering and de-duplication, mutability, and bad elementsparamValueslookup orderServerTestcovers repeated query parameters through the realHttpContext.ParameterNamesTestcompiles a controller at test time with-g,-g:noneand-parameters.Branch history
The branch was rebuilt as these six commits directly on top of
master(6438897), dropping the stale pre-squash copies of #52 and #53 so that "Rebase and merge" works. The final tree is byte-identical to the previous tip (ec6075f).🤖 Generated with Claude Code
https://claude.ai/code/session_01Leqha458EwWbRT5xWiVuyg