Soklet Logo

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();
}

You might want clients to receive a JSON response body like this:

[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();
}

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();
}

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!";
}

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!

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();
}

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.

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();
}

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

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();
}

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

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();
}

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

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();

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();
}

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

Response Marshaling

Hooks are provided for these scenarios:

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();

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";
  }
}

...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
};

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();

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();

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();

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();

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
}

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"
  ]
}

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();

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."
}

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();

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
}

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

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

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();

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

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:

  1. A HEAD must never write a response body
  2. A HEAD must have its Content-Length header set to the number of bytes that the GET response body would have been
  3. A HEAD must pass through whatever HTTP status would have been returned by the GET response body
  4. A HEAD should pass through whatever HTTP headers would have been returned by the GET response

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();

Given this Resource Method:

@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

...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

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();

References:

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
}

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!";
}

301 Moved Permanently

@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!

302 Found

@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!

303 See Other

@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!

307 Temporary Redirect

@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!

308 Permanent Redirect

@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!

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();
}
Previous
Request Handling