Core Concepts
Request Handling
Soklet's primary aim is to map HTTP requests to Plain Old Java Methods (hereafter Resource Methods).
This is a Resource Method that handles GET /hello requests:
@GET("/hello")
public String helloWorld() {
return "Hello, world!";
}
@GET("/hello")
public String helloWorld() {
return "Hello, world!";
}
Soklet detects Resource Methods using a compile-time annotation processor and constructs a lookup table to avoid expensive classpath scans during startup.
When an HTTP request arrives, Soklet determines the appropriate Resource Method to invoke based on HTTP method and URL path pattern matching. Parameter values are injected using the heuristics described in Request Data below.
Resource Methods may return results of any type - Response Writing occurs downstream and is responsible for converting the returned objects to response body bytes sent over the wire.
The Basics
Classes that contain Resource Methods are Plain Old Java Objects - no special interfaces to implement or superclasses to extend. Because Resource Methods may accept and return any types you like, they are amenable to automated testing.
The only requirement is that you decorate your method with an appropriate HTTP annotation like @GET so Soklet's annotation processor can find it.
public class ExampleResource {
// You can name your methods whatever you like
// and return whatever you like (or void for a 204 No Content).
@GET("/")
public String index() {
return "Hello, world!";
}
}
public class ExampleResource {
// You can name your methods whatever you like
// and return whatever you like (or void for a 204 No Content).
@GET("/")
public String index() {
return "Hello, world!";
}
}
Resource Methods must be decorated with one or more of the following annotations:
Each annotation can be repeated. For example:
@GET("/test")
@GET("/test-2")
@POST("/test")
public String test() {
// This responds to the 2 GET URLs and 1 POST URL
return "Multiple methods";
}
@GET("/test")
@GET("/test-2")
@POST("/test")
public String test() {
// This responds to the 2 GET URLs and 1 POST URL
return "Multiple methods";
}
Unless you explicitly specify Resource Methods with @HEAD or @OPTIONS, Soklet will provide sensible default implementations for each.
Your Resource Methods can return any type you like, or void or null or Optional<T>::empty for an HTTP 204. Any Optional<T> instances will be unwrapped.
If you need to specify an HTTP status code, headers, or cookies, you can return an instance of Soklet's Response type.
If you need to say "send exactly these bytes in the response body" (e.g. serving up a binary file or using Servlet Integration), you can return an instance of Soklet's MarshaledResponse type.
// HTTP 204
@GET("/no-op")
public void noOp() {
// Don't do anything
}
// Return any standard JDK type you like...
@GET("/hardcoded-map")
public Map<String, Object> hardcodedMap() {
return Map.of("example", List.of(1, 2, 3));
}
public record AccountResponse (
UUID id,
String emailAddress,
Locale locale
) {}
// ...or define your own.
@GET("/accounts/{id}")
public AccountResponse findAccount(@PathParameter UUID id) {
// Error checking elided
Account account = myBackend.findAccount(id);
return new AccountResponse(
account.getId(),
account.getEmailAddress(),
account.getLocale()
);
}
// Return the Response type if you need fine-grained control
@POST("/detailed-response")
public Response detailedResponse() {
UUID id = myBackend.createRecord();
// HTTP 201 Created
return Response.withStatusCode(201)
.headers(Map.of(
"X-Example", Set.of("testing")
))
.cookies(Set.of(
ResponseCookie.with("test", "abc")
.httpOnly(true)
.secure(true)
.maxAge(Duration.ofMinutes(5))
.sameSite(SameSite.LAX)
.build()
))
.body(Map.of("id", id))
.build();
}
@GET("/example-redirect")
public Response exampleRedirect() {
// Here we use a convenience builder for performing redirects.
// You could alternatively do this "by hand" by setting HTTP status
// and headers appropriately.
return Response.withRedirect(
RedirectType.HTTP_307_TEMPORARY_REDIRECT, "/other-url"
).build();
}
// Use MarshaledResponse to serve up binary data
@GET("/example-image.png")
public MarshaledResponse exampleImage() throws IOException {
Path imageFile = Path.of("/home/user/test.png");
byte[] image = Files.readAllBytes(imageFile);
return MarshaledResponse.withStatusCode(200)
.headers(Map.of(
"Content-Type", Set.of("image/png")
))
.body(image)
.build();
}
// HTTP 204
@GET("/no-op")
public void noOp() {
// Don't do anything
}
// Return any standard JDK type you like...
@GET("/hardcoded-map")
public Map<String, Object> hardcodedMap() {
return Map.of("example", List.of(1, 2, 3));
}
public record AccountResponse (
UUID id,
String emailAddress,
Locale locale
) {}
// ...or define your own.
@GET("/accounts/{id}")
public AccountResponse findAccount(@PathParameter UUID id) {
// Error checking elided
Account account = myBackend.findAccount(id);
return new AccountResponse(
account.getId(),
account.getEmailAddress(),
account.getLocale()
);
}
// Return the Response type if you need fine-grained control
@POST("/detailed-response")
public Response detailedResponse() {
UUID id = myBackend.createRecord();
// HTTP 201 Created
return Response.withStatusCode(201)
.headers(Map.of(
"X-Example", Set.of("testing")
))
.cookies(Set.of(
ResponseCookie.with("test", "abc")
.httpOnly(true)
.secure(true)
.maxAge(Duration.ofMinutes(5))
.sameSite(SameSite.LAX)
.build()
))
.body(Map.of("id", id))
.build();
}
@GET("/example-redirect")
public Response exampleRedirect() {
// Here we use a convenience builder for performing redirects.
// You could alternatively do this "by hand" by setting HTTP status
// and headers appropriately.
return Response.withRedirect(
RedirectType.HTTP_307_TEMPORARY_REDIRECT, "/other-url"
).build();
}
// Use MarshaledResponse to serve up binary data
@GET("/example-image.png")
public MarshaledResponse exampleImage() throws IOException {
Path imageFile = Path.of("/home/user/test.png");
byte[] image = Files.readAllBytes(imageFile);
return MarshaledResponse.withStatusCode(200)
.headers(Map.of(
"Content-Type", Set.of("image/png")
))
.body(image)
.build();
}
How to turn Resource Method return values into a final response body byte-array format for over-the-wire serialization (JSON, Protocol Buffers, Thrift, ...) is generally not the concern of your Resource Method - that happens downstream. See Response Writing for details.
Request Data
Soklet will detect and automatically inject Resource Method parameters of type Request. This enables programmatic access to all aspects of the HTTP request.
@GET("/example")
public void example(Request request /* param name is arbitrary */) {
// Request ID (generated by the configured IdGenerator)
Object requestId = request.getId();
// Here, it would be HttpMethod.GET
HttpMethod httpMethod = request.getHttpMethod();
// The decoded path, e.g. "/example"
String path = request.getPath();
// The path exactly as the client specified,
// e.g. "/a%20b" (never decoded)
String rawPath = request.getRawPath();
// Decoded query parameter values by name
Map<String, Set<String>> queryParameters = request.getQueryParameters();
// Shorthand for plucking the first query param value by name
Optional<String> queryParameter = request.getQueryParameter("test");
// The query exactly as the client specified,
// e.g. "a=b&c=d+e" (never decoded).
// Useful for special cases like HMAC signature
// verification, which might rely on exact client format
Optional<String> rawQuery = request.getRawQuery();
// The path + query exactly as the client specified,
// e.g. "/my%20path?a=b&c=d%20e" (never decoded)
String rawPathAndQuery = request.getRawPathAndQuery();
// Client network address (which might be a proxy), if available
Optional<InetSocketAddress> remoteAddress = request.getRemoteAddress();
// Request body as bytes, if present
Optional<byte[]> body = request.getBody();
// Request body marshaled to a string, if present.
// Charset defined in "Content-Type" header is used to marshal.
// If not specified, UTF-8 is assumed
Optional<String> bodyAsString = request.getBodyAsString();
// Was the request too large to read fully?
boolean contentTooLarge = request.isContentTooLarge();
// Header values by name (names are case-insensitive)
Map<String, Set<String>> headers = request.getHeaders();
// Shorthand for plucking the first header value by name (case-insensitive)
Optional<String> header = request.getHeader("Accept-Language");
// Request cookies by name
Map<String, Set<String>> cookies = request.getCookies();
// Shorthand for plucking the first cookie value by name
Optional<String> cookie = request.getCookie("cookie-name");
// Decoded form parameters by name (application/x-www-form-urlencoded)
Map<String, Set<String>> fps = request.getFormParameters();
// Shorthand for plucking the first form parameter value by name
Optional<String> fp = request.getFormParameter("fp-name");
// Is this a multipart request?
boolean multipart = request.isMultipart();
// Decoded multipart fields by name
Map<String, Set<MultipartField>> mpfs = request.getMultipartFields();
// Shorthand for plucking the first multipart field by name
Optional<MultipartField> mpf = request.getMultipartField("file-input");
// CORS information, if present
Optional<Cors> cors = request.getCors();
// CORS preflight information, if present
Optional<CorsPreflight> corsPreflight = request.getCorsPreflight();
// Ordered locales via Accept-Language parsing
List<Locale> locales = request.getLocales();
// Ordered language ranges via Accept-Language parsing
List<LanguageRange> lrs = request.getLanguageRanges();
// Charset as specified by "Content-Type" header, if present
Optional<Charset> charset = request.getCharset();
// Content type component of "Content-Type" header, if present
Optional<String> contentType = request.getContentType();
}
@GET("/example")
public void example(Request request /* param name is arbitrary */) {
// Request ID (generated by the configured IdGenerator)
Object requestId = request.getId();
// Here, it would be HttpMethod.GET
HttpMethod httpMethod = request.getHttpMethod();
// The decoded path, e.g. "/example"
String path = request.getPath();
// The path exactly as the client specified,
// e.g. "/a%20b" (never decoded)
String rawPath = request.getRawPath();
// Decoded query parameter values by name
Map<String, Set<String>> queryParameters = request.getQueryParameters();
// Shorthand for plucking the first query param value by name
Optional<String> queryParameter = request.getQueryParameter("test");
// The query exactly as the client specified,
// e.g. "a=b&c=d+e" (never decoded).
// Useful for special cases like HMAC signature
// verification, which might rely on exact client format
Optional<String> rawQuery = request.getRawQuery();
// The path + query exactly as the client specified,
// e.g. "/my%20path?a=b&c=d%20e" (never decoded)
String rawPathAndQuery = request.getRawPathAndQuery();
// Client network address (which might be a proxy), if available
Optional<InetSocketAddress> remoteAddress = request.getRemoteAddress();
// Request body as bytes, if present
Optional<byte[]> body = request.getBody();
// Request body marshaled to a string, if present.
// Charset defined in "Content-Type" header is used to marshal.
// If not specified, UTF-8 is assumed
Optional<String> bodyAsString = request.getBodyAsString();
// Was the request too large to read fully?
boolean contentTooLarge = request.isContentTooLarge();
// Header values by name (names are case-insensitive)
Map<String, Set<String>> headers = request.getHeaders();
// Shorthand for plucking the first header value by name (case-insensitive)
Optional<String> header = request.getHeader("Accept-Language");
// Request cookies by name
Map<String, Set<String>> cookies = request.getCookies();
// Shorthand for plucking the first cookie value by name
Optional<String> cookie = request.getCookie("cookie-name");
// Decoded form parameters by name (application/x-www-form-urlencoded)
Map<String, Set<String>> fps = request.getFormParameters();
// Shorthand for plucking the first form parameter value by name
Optional<String> fp = request.getFormParameter("fp-name");
// Is this a multipart request?
boolean multipart = request.isMultipart();
// Decoded multipart fields by name
Map<String, Set<MultipartField>> mpfs = request.getMultipartFields();
// Shorthand for plucking the first multipart field by name
Optional<MultipartField> mpf = request.getMultipartField("file-input");
// CORS information, if present
Optional<Cors> cors = request.getCors();
// CORS preflight information, if present
Optional<CorsPreflight> corsPreflight = request.getCorsPreflight();
// Ordered locales via Accept-Language parsing
List<Locale> locales = request.getLocales();
// Ordered language ranges via Accept-Language parsing
List<LanguageRange> lrs = request.getLanguageRanges();
// Charset as specified by "Content-Type" header, if present
Optional<Charset> charset = request.getCharset();
// Content type component of "Content-Type" header, if present
Optional<String> contentType = request.getContentType();
}
Notes on Ordering and Case-Sensitivity
All Map keysets above are guaranteed to reflect the order in which the data was specified in the request. For example, given a query string test=a&last=b, the keyset's iteration order is test, last.
Further, per HTTP spec, all Map keysets are case-sensitive with the exception of request headers.
For example, these invocations are equivalent:
request.getHeader("Accept-Language")request.getHeader("accept-language")
But these are not!
request.getCookie("Chocolate-Chip")request.getCookie("chocolate-chip")
...and neither are these:
request.getQueryParameter("test")request.getQueryParameter("Test")
Accessing the Request directly is useful for scenarios where you don't know the structure of your requests at compile time.
But for the common case - where you know ahead-of-time what your requests should look like - it's preferable to take advantage of Soklet's Resource Method parameter annotations, which enable static typing via Value Conversions, throw helpful exceptions in the face of invalid input, and improve code reuse/testability by making your Resource Methods be "Just Another Java Method". Read on for details.
Query Parameters
Soklet will inject query parameter values for Resource Method parameters decorated with the @QueryParameter annotation.
// This URL might look like /query-params?date=2022-09-21&value=ABC&value=123
@GET("/query-params")
public String queryParamsExample(
@QueryParameter LocalDate date,
@QueryParameter(name="value", optional=true) List<String> values
) {
return String.format("date=%s, values=%s", date, values);
}
// This URL might look like /query-params?date=2022-09-21&value=ABC&value=123
@GET("/query-params")
public String queryParamsExample(
@QueryParameter LocalDate date,
@QueryParameter(name="value", optional=true) List<String> values
) {
return String.format("date=%s, values=%s", date, values);
}
By default, Soklet will assume the query parameter name is identical to the Java method parameter name. This behavior can be overridden by supplying an explicit name, e.g. @QueryParameter(name="query_param_name"). Query parameter names are case-sensitive.
If the query parameter can be specified multiple times, it must be declared as a List<T>.
If a query parameter is not required, it must have the @QueryParameter(optional=true) annotation element specified or be wrapped in an Optional<T> value, otherwise Soklet will throw a MissingQueryParameterException if not supplied.
MissingQueryParameterException: Required query parameter 'date' was not specified.
MissingQueryParameterException: Required query parameter 'date' was not specified.
If a query parameter is invalid, Soklet will throw an IllegalQueryParameterException.
IllegalQueryParameterException: Illegal value 'xxx' was specified for query parameter 'date' (was expecting a value convertible to class java.time.LocalDate)
IllegalQueryParameterException: Illegal value 'xxx' was specified for query parameter 'date' (was expecting a value convertible to class java.time.LocalDate)
See Value Conversions for details on how Soklet marshals query parameter string values to "complex" types like LocalDate, and how you can customize this behavior.
See Response Writing - Uncaught Exceptions for details on how to catch these exceptions and communicate them back to the client.
Heads Up!
Like most modern servers, Soklet decodes query parameters according to RFC_3986_STRICT semantics. For example, + is never decoded as a space, but %20 would be.
If you have a legacy HTML Form that submits via GET, consider changing to POST to ensure X_WWW_FORM_URLENCODED semantics are used for decoding. See Form Parameters for details.
Path Parameters
Curly-brace syntax is used to denote path parameters.
// This URL might look like /example/123
@GET("/example/{placeholder}")
public Response examplePlaceholder(@PathParameter Integer placeholder) {
return Response.withStatusCode(204)
.headers(
Map.of("X-Placeholder-Header", Set.of(String.valueOf(placeholder)))
)
.build();
}
// This URL might look like /example/123
@GET("/example/{placeholder}")
public Response examplePlaceholder(@PathParameter Integer placeholder) {
return Response.withStatusCode(204)
.headers(
Map.of("X-Placeholder-Header", Set.of(String.valueOf(placeholder)))
)
.build();
}
All parameters in a path must have unique names and should have corresponding @PathParameter-annotated Java method parameters.
By default, Soklet will assume the path parameter name is identical to the Java method parameter name. This behavior can be overridden by supplying an explicit name, e.g. @PathParameter(name="path_param_name"). Path parameter names are case-sensitive.
There is no concept of a missing/optional path parameter. In that scenario, no Resource Method would match the request and processing would fall to Soklet's 404 Not Found handler.
If a path parameter is invalid, Soklet will throw an IllegalPathParameterException.
IllegalPathParameterException: Illegal value 'abc' was specified for path parameter 'placeholder' (was expecting a value convertible to class java.lang.Integer)
IllegalPathParameterException: Illegal value 'abc' was specified for path parameter 'placeholder' (was expecting a value convertible to class java.lang.Integer)
See Value Conversions for details on how Soklet marshals path parameter string values to "complex" types like LocalDate, and how you can customize this behavior.
See Response Writing - Uncaught Exceptions for details on how to catch these exceptions and communicate them back to the client.
Varargs Path Parameters
A special case of @PathParameter is the ability to "collect" a path suffix of arbitrary depth by ending the path parameter name with a * character. This construct, called a Varargs Path Parameter, is useful for serving static files.
For example, this Resource Method path declaration /static/images/{imagePath*} would match paths like:
/static/images/test.jpeg/static/images/flags/pt-BR.png/static/images/deep/path/example.gif
A simple Resource Method that serves image files from disk might look like this:
@GET("/static/images/{imagePath*}")
public MarshaledResponse staticImage(
@PathParameter String imagePath
) throws IOException {
// A directory on the filesystem with images in it
Path imageDirectory = Paths.get("web/public/images");
// Read the file from disk and serve it up.
// Real systems should ensure that malicious 'imagePath' values cannot
// "break out" of the base 'imageDirectory', e.g. '../../../etc/passwd'
return MarshaledResponse.withStatusCode(200)
.headers(Map.of("Content-Type", Set.of(
determineContentType(imagePath).orElse("application/octet-stream")
)))
.body(safelyReadFileFromDirectory(imagePath, imageDirectory))
.build();
}
// Implementation elided
private Optional<String> determineContentType(String path) { ... }
// Implementation elided
private byte[] safelyReadFileFromDirectory(
String path,
Path directory
) throws IOException { ... }
@GET("/static/images/{imagePath*}")
public MarshaledResponse staticImage(
@PathParameter String imagePath
) throws IOException {
// A directory on the filesystem with images in it
Path imageDirectory = Paths.get("web/public/images");
// Read the file from disk and serve it up.
// Real systems should ensure that malicious 'imagePath' values cannot
// "break out" of the base 'imageDirectory', e.g. '../../../etc/passwd'
return MarshaledResponse.withStatusCode(200)
.headers(Map.of("Content-Type", Set.of(
determineContentType(imagePath).orElse("application/octet-stream")
)))
.body(safelyReadFileFromDirectory(imagePath, imageDirectory))
.build();
}
// Implementation elided
private Optional<String> determineContentType(String path) { ... }
// Implementation elided
private byte[] safelyReadFileFromDirectory(
String path,
Path directory
) throws IOException { ... }
Heads Up!
A Resource Method path declaration can have at most one Varargs Path Parameter and, if present, it must be the final path component.
Legal path declarations:
/static/images/{imagePath*}/widgets/{widgetId}/{suffix*}
Illegal path declarations:
/static/{imagePath*}/temp/static/{prefix*}/images/{imagePath*}
Cookies
Soklet will inject cookie values for Resource Method parameters decorated with the @RequestCookie annotation.
@GET("/request-cookies")
public String requestCookiesExample(
@RequestCookie LocalDate date,
@RequestCookie(name="value", optional=true) List<String> values
) {
return String.format("date=%s, values=%s", date, values);
}
@GET("/request-cookies")
public String requestCookiesExample(
@RequestCookie LocalDate date,
@RequestCookie(name="value", optional=true) List<String> values
) {
return String.format("date=%s, values=%s", date, values);
}
By default, Soklet will assume the cookie name is identical to the Java method parameter name. This behavior can be overridden by supplying an explicit name, e.g. @RequestCookie(name="cookie_name"). Cookie names are case-sensitive.
If the cookie can be specified multiple times, it must be declared as a List<T>.
If a cookie is not required, it must have the @RequestCookie(optional=true) annotation element specified or be wrapped in an Optional<T> value, otherwise Soklet will throw a MissingRequestCookieException if not supplied.
MissingRequestCookieException: Required request cookie 'date' was not specified.
MissingRequestCookieException: Required request cookie 'date' was not specified.
If a cookie is invalid, Soklet will throw an IllegalRequestCookieException.
IllegalRequestCookieException: Illegal value 'xxx' was specified for request cookie 'date' (was expecting a value convertible to class java.time.LocalDate)
IllegalRequestCookieException: Illegal value 'xxx' was specified for request cookie 'date' (was expecting a value convertible to class java.time.LocalDate)
See Value Conversions for details on how Soklet marshals request cookie string values to "complex" types like LocalDate, and how you can customize this behavior.
See Response Writing - Uncaught Exceptions for details on how to catch these exceptions and communicate them back to the client.
Headers
Soklet will inject header values for Resource Method parameters decorated with the @RequestHeader annotation.
@GET("/request-headers")
public String requestHeadersExample(
@RequestHeader(name="Accept-Encoding", optional=true) String acceptEncoding
) {
return String.format("Accept-Encoding is %s", acceptEncoding);
}
@GET("/request-headers")
public String requestHeadersExample(
@RequestHeader(name="Accept-Encoding", optional=true) String acceptEncoding
) {
return String.format("Accept-Encoding is %s", acceptEncoding);
}
By default, Soklet will assume the header name is identical to the Java method parameter name. This behavior can be overridden by supplying an explicit name, e.g. @RequestHeader(name="header_name"). Header names are case-insensitive.
If the header can be specified multiple times, it must be declared as a List<T>.
If a header is not required, it must have the @RequestHeader(optional=true) annotation element specified or be wrapped in an Optional<T> value, otherwise Soklet will throw a MissingRequestHeaderException if not supplied.
MissingRequestHeaderException: Required request header 'date' was not specified.
MissingRequestHeaderException: Required request header 'date' was not specified.
If a header is invalid, Soklet will throw an IllegalRequestHeaderException.
IllegalRequestHeaderException: Illegal value 'xxx' was specified for request header 'date' (was expecting a value convertible to class java.time.LocalDate)
IllegalRequestHeaderException: Illegal value 'xxx' was specified for request header 'date' (was expecting a value convertible to class java.time.LocalDate)
See Value Conversions for details on how Soklet marshals request header string values to "complex" types like LocalDate, and how you can customize this behavior.
See Response Writing - Uncaught Exceptions for details on how to catch these exceptions and communicate them back to the client.
Request Body
Soklet will inject the request body for Resource Method parameters decorated with the @RequestBody annotation. The name of the annotated parameter is arbitrary.
String, byte[], and a number of other JDK types as specified in Value Conversions are supported by default.
// Request body as string
@POST("/rb-string")
public void requestBody(@RequestBody String requestBody) {
System.out.println(requestBody);
}
// Use optional=true or Optional<T> if request body is not required
@POST("/rb-optional-string")
public void requestBody(
@RequestBody(optional=true) String optionalRequestBody1,
@RequestBody Optional<String> optionalRequestBody2
) {
// This value can be null
System.out.println(optionalRequestBody1);
// This value is never null, but can be Optional.empty()
System.out.println(optionalRequestBody2);
}
// Can also get access to the raw bytes
@POST("/rb-bytes")
public void requestBody(@RequestBody byte[] requestBody) {
System.out.printf("%d bytes\n", requestBody.length);
}
// Complex types are supported.
// Example: a request body of "2024-10-31" without the quotes
@POST("/rb-date")
public void requestBody(@RequestBody LocalDate date) {
System.out.println(date);
}
// Primitive types are supported.
// Exmaple: a request body of "123" without the quotes
@POST("/rb-primitive-int")
public void requestBody(@RequestBody int primitiveType) {
System.out.println(primitiveType);
}
// Request body as string
@POST("/rb-string")
public void requestBody(@RequestBody String requestBody) {
System.out.println(requestBody);
}
// Use optional=true or Optional<T> if request body is not required
@POST("/rb-optional-string")
public void requestBody(
@RequestBody(optional=true) String optionalRequestBody1,
@RequestBody Optional<String> optionalRequestBody2
) {
// This value can be null
System.out.println(optionalRequestBody1);
// This value is never null, but can be Optional.empty()
System.out.println(optionalRequestBody2);
}
// Can also get access to the raw bytes
@POST("/rb-bytes")
public void requestBody(@RequestBody byte[] requestBody) {
System.out.printf("%d bytes\n", requestBody.length);
}
// Complex types are supported.
// Example: a request body of "2024-10-31" without the quotes
@POST("/rb-date")
public void requestBody(@RequestBody LocalDate date) {
System.out.println(date);
}
// Primitive types are supported.
// Exmaple: a request body of "123" without the quotes
@POST("/rb-primitive-int")
public void requestBody(@RequestBody int primitiveType) {
System.out.println(primitiveType);
}
If the request body is not required, it must have the @RequestBody(optional=true) annotation element specified or be wrapped in an Optional<T> value, otherwise Soklet will throw a MissingRequestBodyException if not supplied.
MissingRequestBodyException: A request body is required for this resource.
MissingRequestBodyException: A request body is required for this resource.
If the request body content is invalid, Soklet will throw an IllegalRequestBodyException.
IllegalRequestBodyException: Illegal value 'xxx' was specified for request body (was expecting a value convertible to class java.time.LocalDate)
IllegalRequestBodyException: Illegal value 'xxx' was specified for request body (was expecting a value convertible to class java.time.LocalDate)
Custom Request Body Parsing
In practice, request bodies are often specially-encoded payloads, e.g. JSON objects or XML documents. Soklet provides a hook for parsing request body types via the RequestBodyMarshaler functional interface:
SokletConfig config = SokletConfig.withServer(
Server.fromPort(8080)
).requestBodyMarshaler(new RequestBodyMarshaler() {
// This example uses Google's GSON
static final Gson GSON = new Gson();
@NonNull
@Override
public Optional<Object> marshalRequestBody(
@NonNull Request request,
@NonNull ResourceMethod resourceMethod,
@NonNull Parameter parameter,
@NonNull Type requestBodyType
) {
// Let GSON turn the request body into an instance
// of the specified type.
//
// Note that this method has access to all runtime information
// about the request, which provides the opportunity to, for example,
// examine annotations on the method/parameter which might
// inform custom marshaling strategies.
return Optional.of(GSON.fromJson(
request.getBodyAsString().orElseThrow(),
requestBodyType
));
}
}).build();
SokletConfig config = SokletConfig.withServer(
Server.fromPort(8080)
).requestBodyMarshaler(new RequestBodyMarshaler() {
// This example uses Google's GSON
static final Gson GSON = new Gson();
@NonNull
@Override
public Optional<Object> marshalRequestBody(
@NonNull Request request,
@NonNull ResourceMethod resourceMethod,
@NonNull Parameter parameter,
@NonNull Type requestBodyType
) {
// Let GSON turn the request body into an instance
// of the specified type.
//
// Note that this method has access to all runtime information
// about the request, which provides the opportunity to, for example,
// examine annotations on the method/parameter which might
// inform custom marshaling strategies.
return Optional.of(GSON.fromJson(
request.getBodyAsString().orElseThrow(),
requestBodyType
));
}
}).build();
With the above marshaler configured, your Resource Methods can now accept JSON via statically-typed @RequestBody parameters. Any construct that Gson knows how to marshal is now available to your code.
@POST("/find-biggest")
public Integer findBiggest(@RequestBody List<Integer> numbers) {
// JSON request body [1,2,3] results in 3 being returned
return Collections.max(numbers);
}
@POST("/find-biggest")
public Integer findBiggest(@RequestBody List<Integer> numbers) {
// JSON request body [1,2,3] results in 3 being returned
return Collections.max(numbers);
}
# Send up request body of [1,2,3]
% curl -i -X POST 'http://localhost:8080/find-biggest' \
-H 'Content-Type: application/json' \
-d '[1,2,3]'
HTTP/1.1 200 OK
Content-Length: 1
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
3
# Send up request body of [1,2,3]
% curl -i -X POST 'http://localhost:8080/find-biggest' \
-H 'Content-Type: application/json' \
-d '[1,2,3]'
HTTP/1.1 200 OK
Content-Length: 1
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
3
Gson also handles POJOs and Record types, which are often used in POST, PUT, and PATCH operations.
public record Employee (
UUID id,
String name
) {}
// Accepts a JSON-formatted Record type as input
@POST("/employees")
public void createEmployee(@RequestBody Employee employee) {
System.out.printf("Create %s\n", employee.name());
}
public record Employee (
UUID id,
String name
) {}
// Accepts a JSON-formatted Record type as input
@POST("/employees")
public void createEmployee(@RequestBody Employee employee) {
System.out.printf("Create %s\n", employee.name());
}
# Send up request body of {"id": "...", "name": "..."}
% curl -i -X POST 'http://localhost:8080/employees' \
-H 'Content-Type: application/json' \
-d '{"id": "734ad459-0ebe-4c64-ba93-1dedf65d4ce2", "name": "Test"}'
HTTP/1.1 204 No Content
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
# Send up request body of {"id": "...", "name": "..."}
% curl -i -X POST 'http://localhost:8080/employees' \
-H 'Content-Type: application/json' \
-d '{"id": "734ad459-0ebe-4c64-ba93-1dedf65d4ce2", "name": "Test"}'
HTTP/1.1 204 No Content
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Don't Need Custom Request Body Handling?
Not every system needs to "speak JSON". You might only require standard URL-Encoded and Multipart forms to consume input from your clients. Soklet has you covered in both cases.
- For working with
application/x-www-form-urlencodedrequests, see Form Parameters. - For working with
multipart/form-datarequests, see Multipart Form Data.
Form Parameters
Soklet natively supports application/x-www-form-urlencoded request encoding, which is a method to transmit non-binary data (e.g. text inputs in an HTML <form>). Form parameter names are case-sensitive.
HTML Form
<form
enctype="application/x-www-form-urlencoded"
action="https://example.soklet.com/form?id=123"
method="POST">
<!-- User can type whatever text they like -->
<input type="number" name="numericValue" />
<!-- Multiple values for the same name are supported -->
<input type="hidden" name="multi" value="1" />
<input type="hidden" name="multi" value="2" />
<!-- Names with special characters can be remapped -->
<textarea name="long-text"></textarea>
<!-- Note: browsers send "on" string to indicate "checked" -->
<input type="checkbox" name="enabled"/>
<input type="submit"/>
</form>
<form
enctype="application/x-www-form-urlencoded"
action="https://example.soklet.com/form?id=123"
method="POST">
<!-- User can type whatever text they like -->
<input type="number" name="numericValue" />
<!-- Multiple values for the same name are supported -->
<input type="hidden" name="multi" value="1" />
<input type="hidden" name="multi" value="2" />
<!-- Names with special characters can be remapped -->
<textarea name="long-text"></textarea>
<!-- Note: browsers send "on" string to indicate "checked" -->
<input type="checkbox" name="enabled"/>
<input type="submit"/>
</form>
Resource Method
@POST("/form")
public MarshaledResponse form(
@QueryParameter Long id,
@FormParameter Integer numericValue,
@FormParameter(optional=true) List<String> multi,
@FormParameter(name="long-text") String longText,
@FormParameter String enabled
) {
// Echo back the inputs
return MarshaledResponse.withStatusCode(200)
.body(
List.of(id, numericValue, multi, longText, enabled).stream()
.map(Object::toString)
.collect(Collectors.joining("\n"))
.getBytes(StandardCharsets.UTF_8)
)
.build();
}
@POST("/form")
public MarshaledResponse form(
@QueryParameter Long id,
@FormParameter Integer numericValue,
@FormParameter(optional=true) List<String> multi,
@FormParameter(name="long-text") String longText,
@FormParameter String enabled
) {
// Echo back the inputs
return MarshaledResponse.withStatusCode(200)
.body(
List.of(id, numericValue, multi, longText, enabled).stream()
.map(Object::toString)
.collect(Collectors.joining("\n"))
.getBytes(StandardCharsets.UTF_8)
)
.build();
}
Test
% curl -i -X POST 'https://example.soklet.com/form?id=123' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'numericValue=456&multi=1&multi=2&long-text=long%20multiline%20text&enabled=on'
HTTP/1.1 200 OK
Content-Length: 37
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
123
456
[1, 2]
long multiline text
on
% curl -i -X POST 'https://example.soklet.com/form?id=123' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'numericValue=456&multi=1&multi=2&long-text=long%20multiline%20text&enabled=on'
HTTP/1.1 200 OK
Content-Length: 37
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
123
456
[1, 2]
long multiline text
on
Multipart Form Data
Soklet natively supports multipart/form-data request encoding, which is a method primarily used to transmit binary files but may include non-binary data as well (e.g. text inputs in an HTML <form>). The MultipartField type provides easy access to filename, content type, and field data in byte[] or String format. Multipart field names are case-sensitive.
HTML Form
<form
enctype="multipart/form-data"
action="https://example.soklet.com/multipart?id=123"
method="POST">
<!-- User can type whatever text they like -->
<input type="text" name="freeform" />
<!-- Multiple values for the same name are supported -->
<input type="hidden" name="multi" value="1" />
<input type="hidden" name="multi" value="2" />
<!-- Prompt user to upload a file -->
<p>
Please attach your document: <input name="doc" type="file" />
</p>
<!-- Multiple file uploads are supported -->
<p>
Supplement 1: <input name="extra" type="file" />
Supplement 2: <input name="extra" type="file" />
</p>
<!-- An optional file -->
<p>
Optionally, attach a photo: <input name="photo" type="file" />
</p>
<input type="submit" value="Upload" />
</form>
<form
enctype="multipart/form-data"
action="https://example.soklet.com/multipart?id=123"
method="POST">
<!-- User can type whatever text they like -->
<input type="text" name="freeform" />
<!-- Multiple values for the same name are supported -->
<input type="hidden" name="multi" value="1" />
<input type="hidden" name="multi" value="2" />
<!-- Prompt user to upload a file -->
<p>
Please attach your document: <input name="doc" type="file" />
</p>
<!-- Multiple file uploads are supported -->
<p>
Supplement 1: <input name="extra" type="file" />
Supplement 2: <input name="extra" type="file" />
</p>
<!-- An optional file -->
<p>
Optionally, attach a photo: <input name="photo" type="file" />
</p>
<input type="submit" value="Upload" />
</form>
Resource Method
@POST("/multipart")
public Response multipart(
@QueryParameter Long id,
// Multipart fields work like other Soklet params
// with support for Optional<T>, List<T>, custom names, ...
@Multipart(optional=true) String freeform,
@Multipart(name="multi") List<Integer> numbers,
// The MultipartField type allows access to additional data,
// like filename and content type (if available).
// The @Multipart annotation is optional
// when your parameter is of type MultipartField...
MultipartField document,
// ...but is useful if you need to massage the name.
@Multipart(name="extra") List<MultipartField> supplements,
// If you specify type byte[] for a @Multipart field,
// you'll get just its binary data injected
@Multipart(optional=true) byte[] photo
) {
// Let's demonstrate the functionality MultipartField provides.
// Form field name, always available, e.g. "document"
String name = document.getName();
// Browser may provide this for files, e.g. "test.pdf"
Optional<String> filename = document.getFilename();
// Browser may provide this for files, e.g. "application/pdf"
Optional<String> contentType = document.getContentType();
// Field data as bytes, if available
Optional<byte[]> data = document.getData();
// Field data as a string, if available
Optional<String> dataAsString = document.getDataAsString();
// Apply the standard redirect-after-POST pattern
return Response.withRedirect(
RedirectType.HTTP_307_TEMPORARY_REDIRECT, "/thanks"
).build();
}
@POST("/multipart")
public Response multipart(
@QueryParameter Long id,
// Multipart fields work like other Soklet params
// with support for Optional<T>, List<T>, custom names, ...
@Multipart(optional=true) String freeform,
@Multipart(name="multi") List<Integer> numbers,
// The MultipartField type allows access to additional data,
// like filename and content type (if available).
// The @Multipart annotation is optional
// when your parameter is of type MultipartField...
MultipartField document,
// ...but is useful if you need to massage the name.
@Multipart(name="extra") List<MultipartField> supplements,
// If you specify type byte[] for a @Multipart field,
// you'll get just its binary data injected
@Multipart(optional=true) byte[] photo
) {
// Let's demonstrate the functionality MultipartField provides.
// Form field name, always available, e.g. "document"
String name = document.getName();
// Browser may provide this for files, e.g. "test.pdf"
Optional<String> filename = document.getFilename();
// Browser may provide this for files, e.g. "application/pdf"
Optional<String> contentType = document.getContentType();
// Field data as bytes, if available
Optional<byte[]> data = document.getData();
// Field data as a string, if available
Optional<String> dataAsString = document.getDataAsString();
// Apply the standard redirect-after-POST pattern
return Response.withRedirect(
RedirectType.HTTP_307_TEMPORARY_REDIRECT, "/thanks"
).build();
}
Soklet provides its own multipart parsing support out-of-the-box. If you'd like to do it yourself, you may supply your own MultipartParser implementation. This is useful if you have specialized requirements or would like to simulate edge-case scenarios for integration tests.
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).multipartParser(new MultipartParser() {
@NonNull
@Override
public Map<String, Set<MultipartField>> extractMultipartFields(
@NonNull Request request
) {
// Inspect the request and return multipart fields
return Map.of();
}
}).build()
).build();
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).multipartParser(new MultipartParser() {
@NonNull
@Override
public Map<String, Set<MultipartField>> extractMultipartFields(
@NonNull Request request
) {
// Inspect the request and return multipart fields
return Map.of();
}
}).build()
).build();
If you write a custom implementation, you can acquire a reference the default instance via MultipartParser::defaultInstance and delegate to it as needed.
Use Caution!
Soklet is designed to power systems that exchange small "transactional" payloads which live entirely in memory. It is not appropriate for handling multipart files at scale, buffering uploads to disk, streaming, etc.
Should you have these requirements, Soklet is better suited for providing clients with a cryptographically-signed upload URL for a third party storage service. Popular and scalable solutions include:
Effective Origin Resolution
Sometimes you need the client-facing origin (scheme + host + optional port) for logging or constructing absolute URLs/redirects. Soklet exposes a utility that derives an effective origin from request headers while letting you decide whether to trust forwarded headers.
// Get a handle to a Request...
Request request = ...;
// ...and extract its effective origin using custom resolution rules.
Optional<String> effectiveOrigin = Utilities.extractEffectiveOrigin(
EffectiveOriginResolver.withRequest(request, TrustPolicy.TRUST_PROXY_ALLOWLIST)
.trustedProxyAddresses(Set.of(
InetAddress.getByName("203.0.113.10")
))
);
// Get a handle to a Request...
Request request = ...;
// ...and extract its effective origin using custom resolution rules.
Optional<String> effectiveOrigin = Utilities.extractEffectiveOrigin(
EffectiveOriginResolver.withRequest(request, TrustPolicy.TRUST_PROXY_ALLOWLIST)
.trustedProxyAddresses(Set.of(
InetAddress.getByName("203.0.113.10")
))
);
Forwarded headers are only used when permitted by the trust policy. If you cannot trust forwarded headers (e.g. Soklet is directly reachable, no load balancer/proxy in front), use TrustPolicy::TRUST_NONE instead. Origin is treated as a fallback signal only (never overrides host). You can also control whether Origin fallback is enabled via EffectiveOriginResolver::allowOriginFallback—if left unset, it is enabled only for TrustPolicy::TRUST_ALL.
Example: Trusted Proxy Allowlist
Input
Remote Address:
203.0.113.10
Headers:
X-Forwarded-Proto: https
X-Forwarded-Host: api.example.com
Host: internal.soklet.local
Trust Policy:
TRUST_PROXY_ALLOWLIST
Trusted Proxy Addresses:
203.0.113.10
Remote Address:
203.0.113.10
Headers:
X-Forwarded-Proto: https
X-Forwarded-Host: api.example.com
Host: internal.soklet.local
Trust Policy:
TRUST_PROXY_ALLOWLIST
Trusted Proxy Addresses:
203.0.113.10
Output
https://api.example.com
https://api.example.com
Example: Untrusted Proxy With Origin Fallback
Input
Remote Address:
198.51.100.20
Headers:
Host: api.example.com
X-Forwarded-Proto: https
X-Forwarded-Host: evil.example.net
Origin: https://api.example.com
Trust Policy:
TRUST_PROXY_ALLOWLIST
Trusted Proxy Addresses:
12.34.100.20
Allow Origin Fallback:
true
Remote Address:
198.51.100.20
Headers:
Host: api.example.com
X-Forwarded-Proto: https
X-Forwarded-Host: evil.example.net
Origin: https://api.example.com
Trust Policy:
TRUST_PROXY_ALLOWLIST
Trusted Proxy Addresses:
12.34.100.20
Allow Origin Fallback:
true
Output
https://api.example.com
https://api.example.com
Example: Forwarded Header (Trust All)
Input
Headers:
Forwarded: proto=https;host=example.com:8443
Trust Policy:
TRUST_ALL
Headers:
Forwarded: proto=https;host=example.com:8443
Trust Policy:
TRUST_ALL
Output
https://example.com:8443
https://example.com:8443
References:
Soklet Types
For easy access, Soklet will inject your Resource Method's Java method parameters with instances that correspond to values pulled from your current SokletConfig. The name of each parameter is arbitrary; matching is done by type.
@GET("/soklet-types")
public void sokletTypes(
// A handle to the current Resource Method (this one!)
ResourceMethod resourceMethod,
// Your configured servers
Server server,
ServerSentEventServer serverSentEventServer,
// Your other configured types
InstanceProvider instanceProvider,
RequestBodyMarshaler requestBodyMarshaler,
ValueConverterRegistry valueConverterRegistry,
ResourceMethodResolver resourceMethodResolver,
RequestInterceptor requestInterceptor,
LifecycleObserver lifecycleObserver,
CorsAuthorizer corsAuthorizer,
ResourceMethodParameterProvider resourceMethodParameterProvider
) {
// Let's use the ServerSentEventServer reference to broadcast an event:
ResourcePath resourcePath =
ResourcePath.fromPath("/chats/123/event-source");
ServerSentEventBroadcaster broadcaster =
serverSentEventServer.acquireBroadcaster(resourcePath).orElseThrow();
// Build the event...
ServerSentEvent event = ServerSentEvent.withEvent("chat-message")
.data("Hello, world")
.build();
// ...and broadcast it to SSE clients
broadcaster.broadcastEvent(event);
}
@GET("/soklet-types")
public void sokletTypes(
// A handle to the current Resource Method (this one!)
ResourceMethod resourceMethod,
// Your configured servers
Server server,
ServerSentEventServer serverSentEventServer,
// Your other configured types
InstanceProvider instanceProvider,
RequestBodyMarshaler requestBodyMarshaler,
ValueConverterRegistry valueConverterRegistry,
ResourceMethodResolver resourceMethodResolver,
RequestInterceptor requestInterceptor,
LifecycleObserver lifecycleObserver,
CorsAuthorizer corsAuthorizer,
ResourceMethodParameterProvider resourceMethodParameterProvider
) {
// Let's use the ServerSentEventServer reference to broadcast an event:
ResourcePath resourcePath =
ResourcePath.fromPath("/chats/123/event-source");
ServerSentEventBroadcaster broadcaster =
serverSentEventServer.acquireBroadcaster(resourcePath).orElseThrow();
// Build the event...
ServerSentEvent event = ServerSentEvent.withEvent("chat-message")
.data("Hello, world")
.build();
// ...and broadcast it to SSE clients
broadcaster.broadcastEvent(event);
}
Other Parameters
If Soklet does not know how to inject a Resource Method parameter, it will ask your InstanceProvider for an instance of one (see Instance Creation for details on how to configure it). A Dependency Injection library like Google Guice can be particularly helpful here.
For example, suppose you have a MyBackend that's required to do some work. Just declare it as a Resource Method parameter and Soklet will take care of injecting it.
// Request body
public record AuthRequest (
String emailAddress,
String password
) {}
// Response body
public record AuthResponse (
String jwt
) {}
// Example backend
public static class MyBackend {
public String acquireJwt(AuthRequest authReq) {
// Error handling elided...
// ...and this is not a real JWT :)
return String.format("%s-JWT", authReq.emailAddress());
}
}
@POST("/example-auth")
public AuthResponse exampleAuth(
@RequestBody AuthRequest authReq,
MyBackend myBackend // vended by your InstanceProvider
) {
// Ask MyBackend for a JWT to carry credentials
String jwt = myBackend.acquireJwt(authReq);
return new AuthResponse(jwt);
}
// Request body
public record AuthRequest (
String emailAddress,
String password
) {}
// Response body
public record AuthResponse (
String jwt
) {}
// Example backend
public static class MyBackend {
public String acquireJwt(AuthRequest authReq) {
// Error handling elided...
// ...and this is not a real JWT :)
return String.format("%s-JWT", authReq.emailAddress());
}
}
@POST("/example-auth")
public AuthResponse exampleAuth(
@RequestBody AuthRequest authReq,
MyBackend myBackend // vended by your InstanceProvider
) {
// Ask MyBackend for a JWT to carry credentials
String jwt = myBackend.acquireJwt(authReq);
return new AuthResponse(jwt);
}
Copying Requests
If you need to adjust a Request (e.g. when rewriting it during request wrapping), use Request::copy to obtain a mutable copier, change the fields you need, then call Request.Copier::finish to build the new instance.
This "copy-to-change" workflow is necessary because Request objects are effectively immutable.
// Prefix the request's path with "/v2", e.g. to aid API versioning
Request updatedRequest = request.copy()
.path(format("/v2%s", request.getPath()))
// Convenience method to get a mutable copy of
// existing headers which you can modify in-place
.headers((Map<String, Set<String>> mutableHeaders) -> {
mutableHeaders.put("X-Another-Header", Set.of("example"));
})
.finish();
// Prefix the request's path with "/v2", e.g. to aid API versioning
Request updatedRequest = request.copy()
.path(format("/v2%s", request.getPath()))
// Convenience method to get a mutable copy of
// existing headers which you can modify in-place
.headers((Map<String, Set<String>> mutableHeaders) -> {
mutableHeaders.put("X-Another-Header", Set.of("example"));
})
.finish();
The copy is shallow for performance (for example, the request body byte array is reused), so treat the original as immutable.
Experts Only
Heads Up!
You will not normally need to customize Resource Method resolution and parameter injection. However, hooks are available to support unique use-cases.
Resource Method Resolution
The ResourceMethodResolver determines how to map HTTP requests to Resource Methods.
Soklet's default implementation will, at compile time, examine annotations like @GET, and at runtime will perform matching on your behalf that "just works".
Ensure Soklet's Annotation Processor Runs
If you see a message like this when running your app:
java.lang.IllegalStateException: No Soklet Resource Methods were found. Please ensure your ResourceMethodResolver is configured correctly.
You likely do not have Soklet's annotation processor configured correctly.
Ensure that you provide javac with these flags:
-parameters-processor com.soklet.SokletProcessor
However, you might need custom behavior to handle special cases. For example, suppose you have public-facing URLs that are frequently mistyped and you'd like to perform fuzzy matching on URL paths to resolve them so long as they are "close enough". Or suppose you have many equivalent URLs that map to the same method - in a way not easily solvable with path parameters - and it's awkward to manually "stack" dozens of @GET annotations on the same method.
Having runtime access to route resolution grants expressive abilities not otherwise achievable by compile-time configuration.
Example Implementation
First, let's make a method we can use to handle HTTP requests. Because we are making a custom ResourceMethodResolver, the method itself does not need a @GET (or similar) annotation.
public class ExampleResource {
// No annotations here
public String hardcodedHandler() {
return "this is hardcoded";
}
}
public class ExampleResource {
// No annotations here
public String hardcodedHandler() {
return "this is hardcoded";
}
}
Now that the class with our resource-handling method is defined, we can configure Soklet with a custom ResourceMethodResolver that routes requests directly to the method, ignoring Soklet's default resolution strategy.
SokletConfig config = SokletConfig.withServer(
Server.fromPort(8080)
).resourceMethodResolver(new ResourceMethodResolver() {
@NonNull
@Override
public Optional<ResourceMethod> resourceMethodForRequest(
@NonNull Request request,
@NonNull ServerType serverType
) {
// If the request was for /hardcoded-404,
// return empty (i.e. no method matches this route)
if (request.getPath().equals("/hardcoded-404"))
return Optional.empty();
try {
// For all other requests, route them directly to
// this "hardcoded" method
Method method = ExampleResource.class.getMethod("hardcodedHandler");
// Make our own resource path declaration from the request's path
ResourcePathDeclaration resourcePathDeclaration =
ResourcePathDeclaration.of(request.getPath());
// We can now construct a Resource Method for this request
return Optional.of(new ResourceMethod(
request.getHttpMethod(), resourcePathDeclaration, method
));
} catch (NoSuchMethodException e) {
throw new IllegalStateException();
}
}
}).build();
SokletConfig config = SokletConfig.withServer(
Server.fromPort(8080)
).resourceMethodResolver(new ResourceMethodResolver() {
@NonNull
@Override
public Optional<ResourceMethod> resourceMethodForRequest(
@NonNull Request request,
@NonNull ServerType serverType
) {
// If the request was for /hardcoded-404,
// return empty (i.e. no method matches this route)
if (request.getPath().equals("/hardcoded-404"))
return Optional.empty();
try {
// For all other requests, route them directly to
// this "hardcoded" method
Method method = ExampleResource.class.getMethod("hardcodedHandler");
// Make our own resource path declaration from the request's path
ResourcePathDeclaration resourcePathDeclaration =
ResourcePathDeclaration.of(request.getPath());
// We can now construct a Resource Method for this request
return Optional.of(new ResourceMethod(
request.getHttpMethod(), resourcePathDeclaration, method
));
} catch (NoSuchMethodException e) {
throw new IllegalStateException();
}
}
}).build();
All /hardcoded-404 requests, regardless of HTTP method, are set up to have no matching route:
% curl -i 'http://localhost:8080/hardcoded-404'
HTTP/1.1 404 Not Found
Content-Length: 19
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
HTTP 404: Not Found
% curl -i 'http://localhost:8080/hardcoded-404'
HTTP/1.1 404 Not Found
Content-Length: 19
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
HTTP 404: Not Found
Any other request will be routed to the hardcoded handler:
% curl -i 'http://localhost:8080/could/be/anything?test=123'
HTTP/1.1 200 OK
Content-Length: 17
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
this is hardcoded
% curl -i 'http://localhost:8080/could/be/anything?test=123'
HTTP/1.1 200 OK
Content-Length: 17
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
this is hardcoded
If you're writing a custom ResourceMethodResolver, it's often useful to get a reference to Soklet's default implementation - for example, you might want custom resolution for a subset of URLs and fall back to standard behavior otherwise:
SokletConfig config = SokletConfig.withServer(
Server.fromPort(8080)
).resourceMethodResolver(new ResourceMethodResolver() {
@NonNull
@Override
public Optional<ResourceMethod> resourceMethodForRequest(
@NonNull Request request,
@NonNull ServerType serverType
) {
// Perform custom resolution if URL path prefix matched
if (request.getPath().startsWith("/some/special/prefix/"))
// Some custom logic here
return ...;
} else {
// Otherwise, fall back to Soklet's default implementation
ResourceMethodResolver defaultRmr =
ResourceMethodResolver.fromClasspathIntrospection();
return defaultRmr.resourceMethodForRequest(request, serverType);
}
}
}).build();
SokletConfig config = SokletConfig.withServer(
Server.fromPort(8080)
).resourceMethodResolver(new ResourceMethodResolver() {
@NonNull
@Override
public Optional<ResourceMethod> resourceMethodForRequest(
@NonNull Request request,
@NonNull ServerType serverType
) {
// Perform custom resolution if URL path prefix matched
if (request.getPath().startsWith("/some/special/prefix/"))
// Some custom logic here
return ...;
} else {
// Otherwise, fall back to Soklet's default implementation
ResourceMethodResolver defaultRmr =
ResourceMethodResolver.fromClasspathIntrospection();
return defaultRmr.resourceMethodForRequest(request, serverType);
}
}
}).build();
References
ResourceMethodResolverResourceMethodResolver::fromClasspathIntrospectionResourceMethodResolver::fromClassesResourceMethodResolver::fromMethodsServerType
Resource Method Parameter Injection
The ResourceMethodParameterProvider determines how to inject appropriate parameter values when invoking Resource Methods.
// This is the contract - given a request and Resource Method,
// supply the parameters to be passed to the Resource Method when invoked
@FunctionalInterface
public interface ResourceMethodParameterProvider {
@NonNull
List<Object> parameterValuesForResourceMethod(
@NonNull Request request,
@NonNull ResourceMethod resourceMethod
);
}
// This is the contract - given a request and Resource Method,
// supply the parameters to be passed to the Resource Method when invoked
@FunctionalInterface
public interface ResourceMethodParameterProvider {
@NonNull
List<Object> parameterValuesForResourceMethod(
@NonNull Request request,
@NonNull ResourceMethod resourceMethod
);
}
Soklet's default implementation uses reflection to examine Resource Method parameter types and annotations and pulls the corresponding values out of the request and applies any needed Value Conversions. If Soklet does not know how to provide a parameter (for example, an un-annotated MyCustomType) Soklet will ask your InstanceProvider for an instance of one.
For example, a Resource Method parameter declared as @RequestBody LocalDate date would trigger the default parameter provider to attempt to convert the request body data to an instance of LocalDate, throwing a MissingRequestBodyException if no request body is available or IllegalRequestBodyException if the request body is malformed.
You can wire in your ResourceMethodParameterProvider like this:
SokletConfig config = SokletConfig.withServer(
Server.fromPort(8080)
).resourceMethodParameterProvider(new ResourceMethodParameterProvider() {
@NonNull
@Override
public List<Object> parameterValuesForResourceMethod(
@NonNull Request request,
@NonNull ResourceMethod resourceMethod
) {
// Your parameter-value-providing code goes here
}
}).build();
SokletConfig config = SokletConfig.withServer(
Server.fromPort(8080)
).resourceMethodParameterProvider(new ResourceMethodParameterProvider() {
@NonNull
@Override
public List<Object> parameterValuesForResourceMethod(
@NonNull Request request,
@NonNull ResourceMethod resourceMethod
) {
// Your parameter-value-providing code goes here
}
}).build();
Heads Up!
It's unusual to provide your own implementation of ResourceMethodParameterProvider.
You can generally achieve the behavior you're looking for providing a custom InstanceProvider - for example, you might want to provide different flavors of MyCustomInstance based on how the parameter is annotated (solving the "robot legs" problem). See Instance Creation for details.

