Core Concepts
Response Writing
Soklet's ResponseMarshaler is responsible for preparing finalized response data to be sent to the client over the wire - it might be the result of a Resource Method that executed successfully which you'd like to marshal to JSON, a friendly representation of an uncaught exception that bubbled out, a custom response for an HTTP 405 Method Not Allowed, and so forth.
This approach enforces a clear separation between your business logic and the details of how data is transformed and written back to clients, which simplifies automated testing and hews to the Single Responsibility Principle.
For example, if your Resource Method looks like this:
@POST("/reverse")
public List<Integer> reverse(@RequestBody List<Integer> numbers) {
return numbers.reversed();
}
@POST("/reverse")
public List<Integer> reverse(@RequestBody List<Integer> numbers) {
return numbers.reversed();
}
You might want clients to receive a JSON response body like this:
[3,2,1]
[3,2,1]
Your ResponseMarshaler is responsible for transforming the List<Integer> data into a "final" byte-array response body representation: an instance of MarshaledResponse.
Writing JSON Responses
Want to see how this looks in practice? Jump to Response Marshaling.
Soklet does not dictate how to encode your data - whether you use JSON or Protocol Buffers or XML or something else is entirely up to you and your application's needs.
If a Resource Method returns an instance of Response, it is provided as-is to your ResponseMarshaler. This permits Resource Methods to explicitly specify response customizations, e.g. explicit status codes, cookies, or other headers.
// This is equivalent to the method above that returns List<Integer>.
// Returning a Response instance enables customizations
@POST("/reverse-again")
public Response reverseAgain(@RequestBody List<Integer> numbers) {
return Response.withStatusCode(200)
.cookies(Set.of( /* any cookies you like */ ))
.headers(Map.of( /* any headers you like */ ))
.body(numbers.reversed())
.build();
}
// This is equivalent to the method above that returns List<Integer>.
// Returning a Response instance enables customizations
@POST("/reverse-again")
public Response reverseAgain(@RequestBody List<Integer> numbers) {
return Response.withStatusCode(200)
.cookies(Set.of( /* any cookies you like */ ))
.headers(Map.of( /* any headers you like */ ))
.body(numbers.reversed())
.build();
}
If a Resource Method returns an instance of MarshaledResponse, your ResponseMarshaler is not invoked because you are indicating "I want to send exactly this content over the wire; no further processing is needed."
This is useful for one-off kinds of responses, e.g. an API that normally serves up JSON but occasionally needs to serve up an image or other binary file:
// Use MarshaledResponse to serve up an image
@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"),
"Content-Length", Set.of(String.valueOf(image.length))
))
.body(image)
.build();
}
// Use MarshaledResponse to serve up an image
@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"),
"Content-Length", Set.of(String.valueOf(image.length))
))
.body(image)
.build();
}
If a Resource Method returns an instance of any other type or void or null, a Response is synthetically created by Soklet and passed along to your ResponseMarshaler.
Response Status
By default, any Resource Method that executes non-exceptionally will return a 200 OK status (or 204 No Content for null values and void return types).
@GET("/status")
public String status() {
return "Hello!";
}
@GET("/status")
public String status() {
return "Hello!";
}
To test:
% curl -i 'http://localhost:8080/status'
HTTP/1.1 200 OK
Content-Length: 6
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello!
% curl -i 'http://localhost:8080/status'
HTTP/1.1 200 OK
Content-Length: 6
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello!
To specify a custom status, your Resource Method should return a Response. Use the Response::withStatusCode builder method to instantiate.
@POST("/widgets")
public Response createWidget(WidgetService widgetService) {
// Hypothetical widget service
widgetService.createWidget();
// Reply with HTTP 201 Created
return Response.withStatusCode(201)
.body("Widget created.")
.build();
}
@POST("/widgets")
public Response createWidget(WidgetService widgetService) {
// Hypothetical widget service
widgetService.createWidget();
// Reply with HTTP 201 Created
return Response.withStatusCode(201)
.body("Widget created.")
.build();
}
To test:
% curl -i -X POST 'http://localhost:8080/widgets'
HTTP/1.1 201 Created
Content-Length: 15
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Widget created.
% curl -i -X POST 'http://localhost:8080/widgets'
HTTP/1.1 201 Created
Content-Length: 15
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Widget created.
There are other scenarios in which response statuses may be set. Examples include:
Generally, your Response Marshaling configuration makes the determination of what status will ultimately go over the wire.
Response Headers
To specify custom response headers, your Resource Method should return a Response constructed with the Response.Builder::headers method.
@GET("/headers")
public Response headers() {
return Response.withStatusCode(200)
.headers(Map.of("One", Set.of("Two", "Three")))
.body("Hello, world")
.build();
}
@GET("/headers")
public Response headers() {
return Response.withStatusCode(200)
.headers(Map.of("One", Set.of("Two", "Three")))
.body("Hello, world")
.build();
}
Because the set of header values were not already sorted, Soklet will apply natural sort ordering (One: Three, Two) in order to maintain consistency across requests.
$ curl -i 'http://localhost:8080/headers'
HTTP/1.1 200 OK
Content-Length: 12
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
One: Three, Two
Hello, world
$ curl -i 'http://localhost:8080/headers'
HTTP/1.1 200 OK
Content-Length: 12
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
One: Three, Two
Hello, world
If header value sort order is important to you, specify either a SortedSet<E> or LinkedHashSet<E>.
@GET("/headers/sorted")
public Response headersSorted() {
// LinkedHashSet preserves insertion order
Set<String> twoAndThree = new LinkedHashSet<>();
twoAndThree.add("Two");
twoAndThree.add("Three");
// TreeSet is naturally ordered
SortedSet<String> fiveAndSix = new TreeSet<>();
fiveAndSix.add("6");
fiveAndSix.add("5");
return Response.withStatusCode(200)
.headers(Map.of(
"One", twoAndThree,
"Four", fiveAndSix
))
.body("Hello, world")
.build();
}
@GET("/headers/sorted")
public Response headersSorted() {
// LinkedHashSet preserves insertion order
Set<String> twoAndThree = new LinkedHashSet<>();
twoAndThree.add("Two");
twoAndThree.add("Three");
// TreeSet is naturally ordered
SortedSet<String> fiveAndSix = new TreeSet<>();
fiveAndSix.add("6");
fiveAndSix.add("5");
return Response.withStatusCode(200)
.headers(Map.of(
"One", twoAndThree,
"Four", fiveAndSix
))
.body("Hello, world")
.build();
}
The header values are then written as Two, Three and 5, 6.
% curl -i 'http://localhost:8080/headers/sorted'
HTTP/1.1 200 OK
Content-Length: 12
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Four: 5, 6
One: Two, Three
Hello, world
% curl -i 'http://localhost:8080/headers/sorted'
HTTP/1.1 200 OK
Content-Length: 12
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Four: 5, 6
One: Two, Three
Hello, world
Response Cookies
To specify response cookies, your Resource Method should return a Response constructed with the Response.Builder::cookies method.
@GET("/current-date")
public Response currentDate(@QueryParameter ZoneId timeZone) {
LocalDate date = LocalDate.now(timeZone);
Instant now = Instant.now();
return Response.withStatusCode(200)
.cookies(Set.of(
ResponseCookie.with("now", String.valueOf(now))
.build(),
ResponseCookie.with("lastDate", date.toString())
.httpOnly(true)
.secure(true)
.maxAge(Duration.ofMinutes(5))
.sameSite(SameSite.LAX)
.build()
))
.build();
}
@GET("/current-date")
public Response currentDate(@QueryParameter ZoneId timeZone) {
LocalDate date = LocalDate.now(timeZone);
Instant now = Instant.now();
return Response.withStatusCode(200)
.cookies(Set.of(
ResponseCookie.with("now", String.valueOf(now))
.build(),
ResponseCookie.with("lastDate", date.toString())
.httpOnly(true)
.secure(true)
.maxAge(Duration.ofMinutes(5))
.sameSite(SameSite.LAX)
.build()
))
.build();
}
To test:
% curl -i 'http://localhost:8080/current-date?timeZone=America/New_York'
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Set-Cookie: lastDate=2024-04-21; Max-Age=300; Secure; HttpOnly; SameSite=Lax
Set-Cookie: now=2024-04-21T16:19:01.128027Z
% curl -i 'http://localhost:8080/current-date?timeZone=America/New_York'
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Set-Cookie: lastDate=2024-04-21; Max-Age=300; Secure; HttpOnly; SameSite=Lax
Set-Cookie: now=2024-04-21T16:19:01.128027Z
To delete a cookie, specify its name and provide a null value:
return Response.withStatusCode(200)
.cookies(Set.of(ResponseCookie.with("lastDate", null).build()))
.build();
return Response.withStatusCode(200)
.cookies(Set.of(ResponseCookie.with("lastDate", null).build()))
.build();
The response cookie construct is provided as a convenience shorthand so you do not have to compute values for Set-Cookie headers yourself. However, if you prefer, you can write the headers directly - see Response Headers for details.
It is unusual but legal to specify multiple response cookies with the same name. Soklet supports this functionality.
By default, Soklet will sort Set-Cookie response headers alphabetically. If sort order for cookies is important to you, specify with either a SortedSet<E> or LinkedHashSet<E>.
@GET("/double-date")
public Response doubleDate(@QueryParameter ZoneId timeZone) {
LocalDate date = LocalDate.now(timeZone);
Instant now = Instant.now();
Instant later = now.plus(1, ChronoUnit.MINUTES);
// Explicitly order cookies, which happen to have the same name.
// Here, put "later" before "now"
Set<ResponseCookie> cookies = new LinkedHashSet<>();
cookies.add(ResponseCookie.with("now", String.valueOf(later)).build());
cookies.add(ResponseCookie.with("now", String.valueOf(now)).build());
return Response.withStatusCode(200)
.cookies(cookies)
.build();
}
@GET("/double-date")
public Response doubleDate(@QueryParameter ZoneId timeZone) {
LocalDate date = LocalDate.now(timeZone);
Instant now = Instant.now();
Instant later = now.plus(1, ChronoUnit.MINUTES);
// Explicitly order cookies, which happen to have the same name.
// Here, put "later" before "now"
Set<ResponseCookie> cookies = new LinkedHashSet<>();
cookies.add(ResponseCookie.with("now", String.valueOf(later)).build());
cookies.add(ResponseCookie.with("now", String.valueOf(now)).build());
return Response.withStatusCode(200)
.cookies(cookies)
.build();
}
To test:
% curl -i 'http://localhost:8080/double-date?timeZone=America/New_York'
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Set-Cookie: now=2024-04-21T16:20:01.706488Z
Set-Cookie: now=2024-04-21T16:19:01.706488Z
% curl -i 'http://localhost:8080/double-date?timeZone=America/New_York'
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Set-Cookie: now=2024-04-21T16:20:01.706488Z
Set-Cookie: now=2024-04-21T16:19:01.706488Z
Response Marshaling
Hooks are provided for these scenarios:
- Resource Method
- Uncaught Exceptions
- 404 Not Found
- 405 Method Not Allowed
- HTTP
OPTIONS - HTTP
HEAD - CORS
- Post-Processing
Normally, you'll want to provide hooks to Soklet's standard ResponseMarshaler implementation (as opposed to implementing the interface yourself) because it provides sensible defaults for things like CORS, OPTIONS, HEAD, and 404s/405s. This way, you can stay focused on how your application writes "normal" Resource Method and uncaught exception responses and let Soklet worry about the rest.
Keep It Simple
Most applications only need to provide handlers for Resource Method and Uncaught Exception scenarios.
But if you'd like to, for example, control how responses are written for HEAD requests - you have the ability to do so. See Advanced Response Marshaling.
Resource Method
This means the request was successfully matched to a Resource Method and executed without throwing an exception.
You have access to the Request, Response, and ResourceMethod that handled the request.
// Let's use Gson to write response body data
// See https://github.com/google/gson
final Gson GSON = new Gson();
// The request was matched to a Resource Method and executed non-exceptionally
ResourceMethodHandler resourceMethodHandler = (
@Nonnull Request request,
@Nonnull Response response,
@Nonnull ResourceMethod resourceMethod
) -> {
// Turn response body into JSON bytes with Gson
Object bodyObject = response.getBody().orElse(null);
byte[] body = bodyObject == null
? null
: GSON.toJson(bodyObject).getBytes(StandardCharsets.UTF_8);
// To be a good citizen, set the Content-Type header
Map<String, Set<String>> headers = new HashMap<>(response.getHeaders());
headers.put("Content-Type", Set.of("application/json;charset=UTF-8"));
// Tell Soklet: "OK - here is the final response data to send"
return MarshaledResponse.withResponse(response)
.headers(headers)
.body(body)
.build();
};
// Provide the default marshaler with our handler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(resourceMethodHandler)
.build()
).build();
// Let's use Gson to write response body data
// See https://github.com/google/gson
final Gson GSON = new Gson();
// The request was matched to a Resource Method and executed non-exceptionally
ResourceMethodHandler resourceMethodHandler = (
@Nonnull Request request,
@Nonnull Response response,
@Nonnull ResourceMethod resourceMethod
) -> {
// Turn response body into JSON bytes with Gson
Object bodyObject = response.getBody().orElse(null);
byte[] body = bodyObject == null
? null
: GSON.toJson(bodyObject).getBytes(StandardCharsets.UTF_8);
// To be a good citizen, set the Content-Type header
Map<String, Set<String>> headers = new HashMap<>(response.getHeaders());
headers.put("Content-Type", Set.of("application/json;charset=UTF-8"));
// Tell Soklet: "OK - here is the final response data to send"
return MarshaledResponse.withResponse(response)
.headers(headers)
.body(body)
.build();
};
// Provide the default marshaler with our handler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(resourceMethodHandler)
.build()
).build();
Having access to the ResourceMethod that handled the request lets you define processing "hints" (among other things) without warping your method's contract.
For example, we can declare a special annotation...
// An annotation that indicates
// "include debugging information in the response"
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Debug {}
public class ExampleResource {
@Debug // Apply the annotation
@GET("/example")
public String example() {
return "hello, world";
}
}
// An annotation that indicates
// "include debugging information in the response"
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Debug {}
public class ExampleResource {
@Debug // Apply the annotation
@GET("/example")
public String example() {
return "hello, world";
}
}
...and then access it via reflection at runtime:
ResourceMethodHandler resourceMethodHandler = (
@Nonnull Request request,
@Nonnull Response response,
@Nonnull ResourceMethod resourceMethod
) -> {
// Prepare standard response headers
Map<String, Set<String>> headers = new HashMap<>(response.getHeaders());
headers.put("Content-Type", Set.of("application/json;charset=UTF-8"));
// If our annotation is present, add some debug headers
if(resourceMethod.getMethod().isAnnotationPresent(Debug.class)) {
headers.put("X-Debug-Id", Set.of(
String.valueOf(request.getId())
));
headers.put("X-Language-Tag", Set.of(
request.getLocales().get(0).toLanguageTag()
));
}
// Rest of method elided
};
ResourceMethodHandler resourceMethodHandler = (
@Nonnull Request request,
@Nonnull Response response,
@Nonnull ResourceMethod resourceMethod
) -> {
// Prepare standard response headers
Map<String, Set<String>> headers = new HashMap<>(response.getHeaders());
headers.put("Content-Type", Set.of("application/json;charset=UTF-8"));
// If our annotation is present, add some debug headers
if(resourceMethod.getMethod().isAnnotationPresent(Debug.class)) {
headers.put("X-Debug-Id", Set.of(
String.valueOf(request.getId())
));
headers.put("X-Language-Tag", Set.of(
request.getLocales().get(0).toLanguageTag()
));
}
// Rest of method elided
};
References:
Uncaught Exceptions
If an exception is thrown while Soklet is processing a request, it is surfaced here so a representation of the error can be written to the response.
Soklet lets you write your applications "normally" - that is, define your own exception types and let them bubble out. There is generally no need to have your application code throw or extend Soklet-specific exceptions.
Because Soklet is responsible for converting request data to the types specified by your Resource Methods via Value Conversions, an abstract BadRequestException and its specific subclasses like IllegalQueryParameterException are available out-of-the-box.
For example, suppose your Resource Method requires a request body, but the client does not provide one. Soklet will throw a MissingRequestBodyException.
The entire set of BadRequestException types is available in the com.soklet.exception package.
// Let's use Gson to write response body data
// See https://github.com/google/gson
final Gson GSON = new Gson();
// Function to create responses for exceptions that bubble out
ThrowableHandler throwableHandler = (
@Nonnull Request request,
@Nonnull Throwable throwable,
@Nullable ResourceMethod resourceMethod
) -> {
// Keep track of what to write to the response
String message;
int statusCode;
// Examine the exception that bubbled out and determine what
// the HTTP status and a user-facing message should be.
// Note: real systems should localize these messages
switch (throwable) {
// Soklet throws this exception - it's a
// specific subclass of BadRequestException
case IllegalQueryParameterException e -> {
message = String.format("Illegal value '%s' for parameter '%s'",
e.getQueryParameterValue().orElse("[not provided]"),
e.getQueryParameterName());
statusCode = 400;
}
// Generically handle other BadRequestExceptions
case BadRequestException ignored -> {
message = "Your request was improperly formatted.";
statusCode = 400;
}
// Something else? Fall back to a 500
default -> {
message = "An unexpected error occurred.";
statusCode = 500;
}
}
// Turn response body into JSON bytes with Gson.
// Note: real systems should expose richer error constructs
// than an object with a single message field
byte[] body = GSON.toJson(Map.of("message", message))
.getBytes(StandardCharsets.UTF_8);
// Specify our headers
Map<String, Set<String>> headers = new HashMap<>();
headers.put("Content-Type", Set.of("application/json;charset=UTF-8"));
return MarshaledResponse.withStatusCode(statusCode)
.headers(headers)
.body(body)
.build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.throwableHandler(throwableHandler)
.build()
).build();
// Let's use Gson to write response body data
// See https://github.com/google/gson
final Gson GSON = new Gson();
// Function to create responses for exceptions that bubble out
ThrowableHandler throwableHandler = (
@Nonnull Request request,
@Nonnull Throwable throwable,
@Nullable ResourceMethod resourceMethod
) -> {
// Keep track of what to write to the response
String message;
int statusCode;
// Examine the exception that bubbled out and determine what
// the HTTP status and a user-facing message should be.
// Note: real systems should localize these messages
switch (throwable) {
// Soklet throws this exception - it's a
// specific subclass of BadRequestException
case IllegalQueryParameterException e -> {
message = String.format("Illegal value '%s' for parameter '%s'",
e.getQueryParameterValue().orElse("[not provided]"),
e.getQueryParameterName());
statusCode = 400;
}
// Generically handle other BadRequestExceptions
case BadRequestException ignored -> {
message = "Your request was improperly formatted.";
statusCode = 400;
}
// Something else? Fall back to a 500
default -> {
message = "An unexpected error occurred.";
statusCode = 500;
}
}
// Turn response body into JSON bytes with Gson.
// Note: real systems should expose richer error constructs
// than an object with a single message field
byte[] body = GSON.toJson(Map.of("message", message))
.getBytes(StandardCharsets.UTF_8);
// Specify our headers
Map<String, Set<String>> headers = new HashMap<>();
headers.put("Content-Type", Set.of("application/json;charset=UTF-8"));
return MarshaledResponse.withStatusCode(statusCode)
.headers(headers)
.body(body)
.build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.throwableHandler(throwableHandler)
.build()
).build();
If an exception bubbles out during execution of ResponseMarshaler::forThrowable, Soklet will create its own "failsafe" response and attempt to write it to the client.
References:
404 Not Found
This is invoked by Soklet if no matching Resource Method was found for the Request.
// Let's use Gson to write response body data
// See https://github.com/google/gson
final Gson GSON = new Gson();
// Function to create responses for when no Resource Method
// was found to service the request
NotFoundHandler notFoundHandler = (
@Nonnull Request request
) -> {
// Specify our headers
Map<String, Set<String>> headers = Map.of(
"Content-Type", Set.of("application/json;charset=UTF-8")
);
// Generate response body JSON as a byte array with Gson.
// Note: real systems should localize this message
String message = "Resource not found.";
byte[] body = GSON.toJson(Map.of("message", message))
.getBytes(StandardCharsets.UTF_8);
return MarshaledResponse.withStatusCode(404)
.headers(headers)
.body(body)
.build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.notFoundHandler(notFoundHandler)
.build()
).build();
// Let's use Gson to write response body data
// See https://github.com/google/gson
final Gson GSON = new Gson();
// Function to create responses for when no Resource Method
// was found to service the request
NotFoundHandler notFoundHandler = (
@Nonnull Request request
) -> {
// Specify our headers
Map<String, Set<String>> headers = Map.of(
"Content-Type", Set.of("application/json;charset=UTF-8")
);
// Generate response body JSON as a byte array with Gson.
// Note: real systems should localize this message
String message = "Resource not found.";
byte[] body = GSON.toJson(Map.of("message", message))
.getBytes(StandardCharsets.UTF_8);
return MarshaledResponse.withStatusCode(404)
.headers(headers)
.body(body)
.build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.notFoundHandler(notFoundHandler)
.build()
).build();
References:
Post-Processing
Soklet provides an optional post-processing hook for final customization before data gets sent over the wire.
// A PostProcessor is applied after other handlers
PostProcessor postProcessor = (
@Nonnull MarshaledResponse marshaledResponse
) -> {
// Copy the response and tack on an 'X-Powered-By' header
return marshaledResponse.copy()
// Copier convenience: acquire a temporarily-mutable copy
// of response headers so you can adjust as needed
.headers((Map<String, Set<String>> mutableHeaders) -> {
mutableHeaders.put("X-Powered-By", Set.of("Soklet"));
})
.finish();
};
// Supply our custom post-processor to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.throwableHandler(...)
.notFoundHandler(...)
.postProcessor(postProcessor)
.build()
).build();
// A PostProcessor is applied after other handlers
PostProcessor postProcessor = (
@Nonnull MarshaledResponse marshaledResponse
) -> {
// Copy the response and tack on an 'X-Powered-By' header
return marshaledResponse.copy()
// Copier convenience: acquire a temporarily-mutable copy
// of response headers so you can adjust as needed
.headers((Map<String, Set<String>> mutableHeaders) -> {
mutableHeaders.put("X-Powered-By", Set.of("Soklet"));
})
.finish();
};
// Supply our custom post-processor to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.throwableHandler(...)
.notFoundHandler(...)
.postProcessor(postProcessor)
.build()
).build();
References:
Advanced Response Marshaling
Experts Only
You can normally use the out-of-the-box implementation provided by the ResponseMarshaler::withDefaults for the below scenarios instead of writing your own.
405 Method Not Allowed
This is invoked by Soklet if a Resource Method was matched for the Request, but the HTTP Method is inapplicable.
Here is an example of a custom implementation:
// Let's use Gson to write response body data
// See https://github.com/google/gson
final Gson GSON = new Gson();
MethodNotAllowedHandler methodNotAllowedHandler = (
@Nonnull Request request,
@Nonnull Set<HttpMethod> allowedHttpMethods
) -> {
// Soklet provides the set of permitted HTTP methods
Set<String> allowedHttpMethodsAsStrings = allowedHttpMethods.stream()
.map(httpMethod -> httpMethod.name())
.collect(Collectors.toCollection(LinkedHashSet::new));
// Ensure we set the "Allow" header
Map<String, Set<String>> headers = Map.of(
"Allow", allowedHttpMethodsAsStrings,
"Content-Type", Set.of("application/json;charset=UTF-8")
);
// This response body tells callers what is expected
byte[] body = GSON.toJson(Map.of(
// Note: real systems should localize this message
"message", "Method not allowed.",
"requested", request.getHttpMethod().name(),
"allowed", allowedHttpMethodsAsStrings
)).getBytes(StandardCharsets.UTF_8);
return MarshaledResponse.withStatusCode(405)
.headers(headers)
.body(body)
.build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.methodNotAllowedHandler(methodNotAllowedHandler)
.build()
).build();
// Let's use Gson to write response body data
// See https://github.com/google/gson
final Gson GSON = new Gson();
MethodNotAllowedHandler methodNotAllowedHandler = (
@Nonnull Request request,
@Nonnull Set<HttpMethod> allowedHttpMethods
) -> {
// Soklet provides the set of permitted HTTP methods
Set<String> allowedHttpMethodsAsStrings = allowedHttpMethods.stream()
.map(httpMethod -> httpMethod.name())
.collect(Collectors.toCollection(LinkedHashSet::new));
// Ensure we set the "Allow" header
Map<String, Set<String>> headers = Map.of(
"Allow", allowedHttpMethodsAsStrings,
"Content-Type", Set.of("application/json;charset=UTF-8")
);
// This response body tells callers what is expected
byte[] body = GSON.toJson(Map.of(
// Note: real systems should localize this message
"message", "Method not allowed.",
"requested", request.getHttpMethod().name(),
"allowed", allowedHttpMethodsAsStrings
)).getBytes(StandardCharsets.UTF_8);
return MarshaledResponse.withStatusCode(405)
.headers(headers)
.body(body)
.build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.methodNotAllowedHandler(methodNotAllowedHandler)
.build()
).build();
For example, suppose we configure a Resource Method to handle PUT, POST, and DELETE.
@PUT("/example")
@POST("/example")
@DELETE("/example")
public void example405() {
// Implementation doesn't matter
}
@PUT("/example")
@POST("/example")
@DELETE("/example")
public void example405() {
// Implementation doesn't matter
}
Accessing it with a GET and the above ResponseMarshaler will result in this response:
% curl -i 'http://localhost:8080/example'
HTTP/1.1 405 Method Not Allowed
Allow: DELETE, POST, PUT
Content-Length: 116
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
{
"message": "Method not allowed.",
"requested": "GET",
"allowed": [
"DELETE",
"POST",
"PUT"
]
}
% curl -i 'http://localhost:8080/example'
HTTP/1.1 405 Method Not Allowed
Allow: DELETE, POST, PUT
Content-Length: 116
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
{
"message": "Method not allowed.",
"requested": "GET",
"allowed": [
"DELETE",
"POST",
"PUT"
]
}
References:
413 Content Too Large
This is invoked by Soklet if the size of the Request is above the maximum size configured for the server. These scenarios are usually triggered by a multipart file upload or an otherwise unusually large request body.
You can detect "content too large" status by querying Request::isContentTooLarge.
Use Caution
Depending on when the "content too large" state was reached, Request might contain incomplete sets of headers/cookies. It will always have a zero-length body.
Soklet is designed to power systems that exchange small "transactional" payloads that live entirely in memory. It is not appropriate for handling multipart files at scale, buffering uploads to disk, streaming, etc.
Explicitly handling 413 Content Too Large is an unusual case.
Here is an example of a custom implementation:
// Let's use Gson to write response body data
// See https://github.com/google/gson
final Gson GSON = new Gson();
ContentTooLargeHandler contentTooLargeHandler = (
@Nonnull Request request,
@Nullable ResourceMethod resourceMethod
) -> {
// Specify our headers
Map<String, Set<String>> headers = Map.of(
"Content-Type", Set.of("application/json;charset=UTF-8")
);
// Turn response body into JSON bytes with Gson.
// Note: real systems should localize this message
String message = "Request was too large.";
byte[] body = GSON.toJson(Map.of("message", message))
.getBytes(StandardCharsets.UTF_8);
return MarshaledResponse.withStatusCode(413)
.headers(headers)
.body(body)
.build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.contentTooLargeHandler(contentTooLargeHandler)
.build()
).build();
// Let's use Gson to write response body data
// See https://github.com/google/gson
final Gson GSON = new Gson();
ContentTooLargeHandler contentTooLargeHandler = (
@Nonnull Request request,
@Nullable ResourceMethod resourceMethod
) -> {
// Specify our headers
Map<String, Set<String>> headers = Map.of(
"Content-Type", Set.of("application/json;charset=UTF-8")
);
// Turn response body into JSON bytes with Gson.
// Note: real systems should localize this message
String message = "Request was too large.";
byte[] body = GSON.toJson(Map.of("message", message))
.getBytes(StandardCharsets.UTF_8);
return MarshaledResponse.withStatusCode(413)
.headers(headers)
.body(body)
.build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.contentTooLargeHandler(contentTooLargeHandler)
.build()
).build();
Exercising by including a large file in the request body results in this response:
% curl -i -d '@very-big-file.mp4' 'http://localhost:8080/big-file'
HTTP/1.1 413 Content Too Large
Content-Length: 41
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
{
"message": "Request was too large."
}
% curl -i -d '@very-big-file.mp4' 'http://localhost:8080/big-file'
HTTP/1.1 413 Content Too Large
Content-Length: 41
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
{
"message": "Request was too large."
}
References:
HTTP OPTIONS
This is invoked by Soklet if a Resource Method was matched for the Request and the HTTP Method is OPTIONS. Special handling is also available for OPTIONS * requests, which are not tied to Resource Methods.
Experts Only
If you're looking to customize OPTIONS handling, it's likely that you want to manually handle CORS scenarios.
Soklet already provides comprehensive CORS support. But if you prefer to roll your own, read on.
Here is an example of a custom implementation:
OptionsHandler optionsHandler = (
@Nonnull Request request,
@Nonnull Set<HttpMethod> allowedHttpMethods
) -> {
// Soklet examines your Resource Methods and provides you with
// the set of HTTP methods supported for the request.
// This enables you to easily write the "Allow" response header
Set<String> allowedHttpMethodsAsStrings = allowedHttpMethods.stream()
.map(httpMethod -> httpMethod.name())
.collect(Collectors.toSet());
// Normally OPTIONS is a 204 with an Allow header
return MarshaledResponse.withStatusCode(204)
.headers(Map.of("Allow", allowedHttpMethodsAsStrings))
.build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.optionsHandler(optionsHandler)
.build()
).build();
OptionsHandler optionsHandler = (
@Nonnull Request request,
@Nonnull Set<HttpMethod> allowedHttpMethods
) -> {
// Soklet examines your Resource Methods and provides you with
// the set of HTTP methods supported for the request.
// This enables you to easily write the "Allow" response header
Set<String> allowedHttpMethodsAsStrings = allowedHttpMethods.stream()
.map(httpMethod -> httpMethod.name())
.collect(Collectors.toSet());
// Normally OPTIONS is a 204 with an Allow header
return MarshaledResponse.withStatusCode(204)
.headers(Map.of("Allow", allowedHttpMethodsAsStrings))
.build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.optionsHandler(optionsHandler)
.build()
).build();
For example, suppose we configure a Resource Method to handle PUT, POST, and DELETE.
@PUT("/example")
@POST("/example")
@DELETE("/example")
public void exampleOptions() {
// Implementation doesn't matter
}
@PUT("/example")
@POST("/example")
@DELETE("/example")
public void exampleOptions() {
// Implementation doesn't matter
}
Accessing it with via OPTIONS and the above customized ResponseMarshaler will result in this response:
% curl -i -X OPTIONS 'http://localhost:8080/example'
HTTP/1.1 204 No Content
Allow: DELETE
Allow: OPTIONS
Allow: POST
Allow: PUT
Content-Length: 0
Date: Sun, 21 Mar 2024 16:19:01 GMT
% curl -i -X OPTIONS 'http://localhost:8080/example'
HTTP/1.1 204 No Content
Allow: DELETE
Allow: OPTIONS
Allow: POST
Allow: PUT
Content-Length: 0
Date: Sun, 21 Mar 2024 16:19:01 GMT
It's also possible for clients to issue a special OPTIONS * request (colloquially, OPTIONS Splat). This is a special HTTP/1.1 request defined in RFC 7231 which permits querying of server-wide capabilities, not the capabilities of a particular resource. For example, a load balancer might want to ask "is the system up?" without hitting an explicit health-check URL.
By default, Soklet will write an HTTP 200 OK response for OPTIONS * requests like this:
% curl -X OPTIONS --request-target '*' 'http://localhost:8080' -i
HTTP/1.1 200 OK
Allow: DELETE
Allow: GET
Allow: HEAD
Allow: OPTIONS
Allow: PATCH
Allow: POST
Allow: PUT
Content-Length: 0
Date: Sun, 21 Mar 2024 16:19:01 GMT
% curl -X OPTIONS --request-target '*' 'http://localhost:8080' -i
HTTP/1.1 200 OK
Allow: DELETE
Allow: GET
Allow: HEAD
Allow: OPTIONS
Allow: PATCH
Allow: POST
Allow: PUT
Content-Length: 0
Date: Sun, 21 Mar 2024 16:19:01 GMT
You may override this behavior by providing an OptionsSplatHandler:
OptionsSplatHandler optionsSplatHandler = (
@Nonnull Request request
) -> {
// Expose a subset of HTTP methods in the Allow header
SortedSet<String> allowedHttpMethods =
new TreeSet<>(List.of("GET", "OPTIONS", "POST"));
// Normally OPTIONS * is a 200 with an Allow header,
// we add an extra 'Server' header here
return MarshaledResponse.withStatusCode(200)
.headers(Map.of(
"Allow", allowedHttpMethods,
"Server", Set.of("Soklet")
)).build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.optionsSplatHandler(optionsSplatHandler)
.build()
).build();
OptionsSplatHandler optionsSplatHandler = (
@Nonnull Request request
) -> {
// Expose a subset of HTTP methods in the Allow header
SortedSet<String> allowedHttpMethods =
new TreeSet<>(List.of("GET", "OPTIONS", "POST"));
// Normally OPTIONS * is a 200 with an Allow header,
// we add an extra 'Server' header here
return MarshaledResponse.withStatusCode(200)
.headers(Map.of(
"Allow", allowedHttpMethods,
"Server", Set.of("Soklet")
)).build();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.optionsSplatHandler(optionsSplatHandler)
.build()
).build();
With the custom handler, OPTIONS * responses now look like this:
% curl -X OPTIONS --request-target '*' 'http://localhost:8080' -i
HTTP/1.1 200 OK
Allow: GET
Allow: OPTIONS
Allow: POST
Content-Length: 0
Date: Sun, 21 Mar 2024 16:19:01 GMT
Server: Soklet
% curl -X OPTIONS --request-target '*' 'http://localhost:8080' -i
HTTP/1.1 200 OK
Allow: GET
Allow: OPTIONS
Allow: POST
Content-Length: 0
Date: Sun, 21 Mar 2024 16:19:01 GMT
Server: Soklet
References:
HTTP HEAD
This is invoked by Soklet if a Resource Method was matched for the Request and the HTTP Method is HEAD. The idea behind HEAD is for the client to determine the size of the response body that would be returned by a GET request but without the server providing the body.
Four rules:
- A
HEADmust never write a response body - A
HEADmust have itsContent-Lengthheader set to the number of bytes that theGETresponse body would have been - A
HEADmust pass through whatever HTTP status would have been returned by theGETresponse body - A
HEADshould pass through whatever HTTP headers would have been returned by theGETresponse
Here is an example of a custom implementation (which is essentially identical to Soklet's default implementation):
HeadHandler headHandler = (
@Nonnull Request request,
@Nonnull MarshaledResponse getMethodMarshaledResponse
) -> {
// Soklet automatically marshals the GET response for you
byte[] getResponseBytes = getMethodMarshaledResponse.getBody()
.orElse(new byte[] {});
// MarshaledResponse is immutable so we make a mutable copy
// of the GET response to construct our HEAD response.
return getMethodMarshaledResponse.copy()
// Ensure no response body bytes are written
.body(null)
.cookies(getMethodMarshaledResponse.getCookies())
.headers((mutableHeaders) -> {
// For convenience, Soklet provides a mutable represention
// of the response headers when using the copy builder.
// Here, we specify our own Content-Length value using
// the number of GET response bytes
mutableHeaders.put("Content-Length", Set.of(
String.valueOf(getResponseBytes.length)
));
})
.finish();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.headHandler(headHandler)
.build()
).build();
HeadHandler headHandler = (
@Nonnull Request request,
@Nonnull MarshaledResponse getMethodMarshaledResponse
) -> {
// Soklet automatically marshals the GET response for you
byte[] getResponseBytes = getMethodMarshaledResponse.getBody()
.orElse(new byte[] {});
// MarshaledResponse is immutable so we make a mutable copy
// of the GET response to construct our HEAD response.
return getMethodMarshaledResponse.copy()
// Ensure no response body bytes are written
.body(null)
.cookies(getMethodMarshaledResponse.getCookies())
.headers((mutableHeaders) -> {
// For convenience, Soklet provides a mutable represention
// of the response headers when using the copy builder.
// Here, we specify our own Content-Length value using
// the number of GET response bytes
mutableHeaders.put("Content-Length", Set.of(
String.valueOf(getResponseBytes.length)
));
})
.finish();
};
// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.headHandler(headHandler)
.build()
).build();
Given this Resource Method:
@GET("/hello")
public String hello() {
return "world";
}
@GET("/hello")
public String hello() {
return "world";
}
...here's how a GET might look:
% curl -i -X GET 'http://localhost:8080/hello'
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
world
% curl -i -X GET 'http://localhost:8080/hello'
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
world
...and here's how a HEAD might look:
% curl --head -i 'http://localhost:8080/hello'
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
% curl --head -i 'http://localhost:8080/hello'
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
References:
CORS
If Soklet sees an Origin header is specified in the Request - that is, it appears to be a CORS request - it consults its configured CorsAuthorizer, which provides preflight requests with a thumbs-up or thumbs-down and supplements approved requests with additional data in the response headers.
Experts Only
Unless you have special requirements, you should prefer to use Soklet's CORS support instead of writing your own.
Please refer to Soklet's CORS documentation for a comprehensive treatment of CORS, from basic whitelisting of permitted domains to full customization of authorization and response writing.
Should you prefer to roll your own CORS responses, the hooks below are available to you:
// Preflight: the request is allowed
CorsPreflightAllowedHandler corsPreflightAllowedHandler = (
@Nonnull Request request,
@Nonnull CorsPreflight corsPreflight,
@Nonnull CorsPreflightResponse corsPreflightResponse
) -> {
// Return a MarshaledResponse with headers like
// Access-Control-Allow-Origin, Vary, etc.
};
// Preflight: the request is not allowed
CorsPreflightRejectedHandler corsPreflightRejectedHandler = (
@Nonnull Request request,
@Nonnull CorsPreflight corsPreflight
) -> {
// Return a MarshaledResponse with 403 or similar status
};
// Supplement responses for requests that have "passed" preflight
CorsAllowedHandler corsAllowedHandler = (
@Nonnull Request request,
@Nonnull Cors cors,
@Nonnull CorsResponse corsResponse,
@Nonnull MarshaledResponse marshaledResponse
) -> {
// Supplement the provided MarshaledResponse with
// Access-Control-Allow-Origin, Vary, etc. and return it
};
// Supply our custom handlers to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.corsPreflightAllowedHandler(corsPreflightAllowedHandler)
.corsPreflightRejectedHandler(corsPreflightRejectedHandler)
.corsAllowedHandler(corsAllowedHandler)
.build()
).build();
// Preflight: the request is allowed
CorsPreflightAllowedHandler corsPreflightAllowedHandler = (
@Nonnull Request request,
@Nonnull CorsPreflight corsPreflight,
@Nonnull CorsPreflightResponse corsPreflightResponse
) -> {
// Return a MarshaledResponse with headers like
// Access-Control-Allow-Origin, Vary, etc.
};
// Preflight: the request is not allowed
CorsPreflightRejectedHandler corsPreflightRejectedHandler = (
@Nonnull Request request,
@Nonnull CorsPreflight corsPreflight
) -> {
// Return a MarshaledResponse with 403 or similar status
};
// Supplement responses for requests that have "passed" preflight
CorsAllowedHandler corsAllowedHandler = (
@Nonnull Request request,
@Nonnull Cors cors,
@Nonnull CorsResponse corsResponse,
@Nonnull MarshaledResponse marshaledResponse
) -> {
// Supplement the provided MarshaledResponse with
// Access-Control-Allow-Origin, Vary, etc. and return it
};
// Supply our custom handlers to the marshaler
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).responseMarshaler(
ResponseMarshaler.withDefaults()
.resourceMethodHandler(...)
.corsPreflightAllowedHandler(corsPreflightAllowedHandler)
.corsPreflightRejectedHandler(corsPreflightRejectedHandler)
.corsAllowedHandler(corsAllowedHandler)
.build()
).build();
References:
CorsPreflightAllowedHandlerResponseMarshaler::forCorsPreflightAllowedCorsPreflightRejectedHandlerResponseMarshaler::forCorsPreflightRejectedCorsAllowedHandlerResponseMarshaler::forCorsAllowed
Full Customization
You may directly implement the ResponseMarshaler interface and provide it to Soklet as shown below. This is unusual; most applications are better served by providing "handler" functions to Soklet's internal implementation as outlined above.
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
// Bring your own totally-custom response marshaler
).responseMarshaler(new ResponseMarshaler() {
@Nonnull
@Override
public MarshaledResponse forResourceMethod(
@Nonnull Request request,
@Nonnull Response response,
@Nonnull ResourceMethod resourceMethod
) {
// TODO: handle standard path
}
@Nonnull
@Override
public MarshaledResponse forThrowable(
@Nonnull Request request,
@Nonnull Throwable throwable,
@Nullable ResourceMethod resourceMethod
) {
// TODO: handle exception path
}
// Other ResponseMarshaler methods elided
}
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
// Bring your own totally-custom response marshaler
).responseMarshaler(new ResponseMarshaler() {
@Nonnull
@Override
public MarshaledResponse forResourceMethod(
@Nonnull Request request,
@Nonnull Response response,
@Nonnull ResourceMethod resourceMethod
) {
// TODO: handle standard path
}
@Nonnull
@Override
public MarshaledResponse forThrowable(
@Nonnull Request request,
@Nonnull Throwable throwable,
@Nullable ResourceMethod resourceMethod
) {
// TODO: handle exception path
}
// Other ResponseMarshaler methods elided
}
References:
Redirects
Soklet provides special convenience shorthand for the following types of HTTP Redirect:
For the below examples, assume they will redirect to a GET /new Resource Method.
@GET("/new")
public String redirectTarget() {
return "Hello from /new!";
}
@GET("/new")
public String redirectTarget() {
return "Hello from /new!";
}
301 Moved Permanently
@GET("/test/301")
public Response test301() {
return Response.withRedirect(
RedirectType.HTTP_301_MOVED_PERMANENTLY, "/new"
).build();
}
@GET("/test/301")
public Response test301() {
return Response.withRedirect(
RedirectType.HTTP_301_MOVED_PERMANENTLY, "/new"
).build();
}
To test:
% curl -L -i 'http://localhost:8080/test/301'
HTTP/1.1 301 Moved Permanently
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Location: /new
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello from /new!
% curl -L -i 'http://localhost:8080/test/301'
HTTP/1.1 301 Moved Permanently
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Location: /new
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello from /new!
302 Found
@GET("/test/302")
public Response test302() {
return Response.withRedirect(
RedirectType.HTTP_302_FOUND, "/new"
).build();
}
@GET("/test/302")
public Response test302() {
return Response.withRedirect(
RedirectType.HTTP_302_FOUND, "/new"
).build();
}
To test:
% curl -L -i 'http://localhost:8080/test/302'
HTTP/1.1 302 Found
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Location: /new
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello from /new!
% curl -L -i 'http://localhost:8080/test/302'
HTTP/1.1 302 Found
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Location: /new
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello from /new!
303 See Other
@GET("/test/303")
public Response test303() {
return Response.withRedirect(
RedirectType.HTTP_303_SEE_OTHER, "/new"
).build();
}
@GET("/test/303")
public Response test303() {
return Response.withRedirect(
RedirectType.HTTP_303_SEE_OTHER, "/new"
).build();
}
To test:
% curl -L -i 'http://localhost:8080/test/303'
HTTP/1.1 303 See Other
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Location: /new
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello from /new!
% curl -L -i 'http://localhost:8080/test/303'
HTTP/1.1 303 See Other
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Location: /new
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello from /new!
307 Temporary Redirect
@GET("/test/307")
public Response test307() {
return Response.withRedirect(
RedirectType.HTTP_307_TEMPORARY_REDIRECT, "/new"
).build();
}
@GET("/test/307")
public Response test307() {
return Response.withRedirect(
RedirectType.HTTP_307_TEMPORARY_REDIRECT, "/new"
).build();
}
To test:
% curl -L -i 'http://localhost:8080/test/307'
HTTP/1.1 307 Temporary Redirect
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Location: /new
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello from /new!
% curl -L -i 'http://localhost:8080/test/307'
HTTP/1.1 307 Temporary Redirect
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Location: /new
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello from /new!
308 Permanent Redirect
@GET("/test/308")
public Response test308() {
return Response.withRedirect(
RedirectType.HTTP_308_PERMANENT_REDIRECT, "/new"
).build();
}
@GET("/test/308")
public Response test308() {
return Response.withRedirect(
RedirectType.HTTP_308_PERMANENT_REDIRECT, "/new"
).build();
}
To test:
% curl -L -i 'http://localhost:8080/test/308'
HTTP/1.1 308 Permanent Redirect
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Location: /new
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello from /new!
% curl -L -i 'http://localhost:8080/test/308'
HTTP/1.1 308 Permanent Redirect
Content-Length: 0
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Location: /new
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello from /new!
Custom
It's not required to use Soklet's RedirectType shorthand for redirects - manually setting the HTTP status code and Location response header works just as well.
@GET("/test/307-custom")
public Response test307Custom() {
return Response.withStatusCode(307)
.headers(Map.of("Location", Set.of("/new")))
.build();
}
@GET("/test/307-custom")
public Response test307Custom() {
return Response.withStatusCode(307)
.headers(Map.of("Location", Set.of("/new")))
.build();
}

