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 finalized response representation: an instance of MarshaledResponse, optionally with a MarshaledResponseBody.

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() {
  Path imageFile = Path.of("/home/user/test.png");

  return MarshaledResponse.withStatusCode(200)
    .body(imageFile)
    .headers(Map.of(
      "Content-Type", Set.of("image/png")
    ))
    .build();
}

Serving Files Efficiently

Want to see how file, file-channel, and pre-rendered buffer responses work? Jump to Zero-Copy Responses.

Streaming Dynamic Responses

For dynamic output where the final length is not known upfront, return a MarshaledResponse with a StreamingResponseBody. See Streaming Responses.

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
One: 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
Four: 6
One: Two
One: Three

Hello, world

Parameterized Header Values

Some response headers use a special "value; param=value" format (for example, Content-Disposition and Content-Type), which are more nuanced than they might first appear.

Soklet provides ParameterizedHeaderValue to build these safely with strict RFC encoding rules. Invalid tokens or control characters will fail fast, preventing you from creating out-of-spec responses.

All 3 parameter types are supported:

@GET("/resume.pdf")
public MarshaledResponse resume() {
  Path resumeFile = Path.of("/home/user/resume.pdf");

  // Construct our "attachment=..." Content-Disposition value
  String contentDisposition = ParameterizedHeaderValue.withName("attachment")
    .quotedParameter("filename", "resume.pdf")
    .rfc8187Parameter("filename", "résumé.pdf")
    .stringValue();

  return MarshaledResponse.withStatusCode(200)
    .body(resumeFile)
    .headers(Map.of(
      "Content-Disposition", Set.of(contentDisposition),
      "Content-Type", Set.of("application/pdf")
    ))
    .build();
}

The resulting Content-Disposition header value is:

attachment; filename="resume.pdf"; filename*=UTF-8''r%C3%A9sum%C3%A9.pdf

Which Type[s] Should I Use?

Use the narrowest one that fits your data:

  • tokenParameter for strict RFC 9110 tokens (ASCII only, no spaces, no quotes). Good for values like charset=UTF-8.
  • quotedParameter for ASCII values that may include spaces or punctuation; Soklet will quote and escape as needed.
  • rfc8187Parameter for non-ASCII values; Soklet encodes UTF-8 and percent-escapes it per RFC 8187.

For legacy compatibility (notably Content-Disposition filenames), send both a quotedParameter with a safe ASCII fallback and an rfc8187Parameter with the full Unicode value.

Modern clients prefer filename*, while older ones only understand filename.

Response Cookies

To specify response cookies, your Resource Method should return a Response constructed with a set of ResponseCookie.

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

Soklet supports the full Set-Cookie attribute surface, including Expires, Priority, and Partitioned:

ResponseCookie sessionCookie = ResponseCookie.with("session", token)
  .expires(Instant.now().plus(Duration.ofDays(7)))
  .priority(Priority.HIGH)
  .partitioned(true)
  .secure(true)
  .sameSite(SameSite.NONE)
  .path("/")
  .build();

Validation

Soklet validates ResponseCookie instances at construction time and fails fast when invalid. For example:

  • Max-Age must be >= 0
  • SameSite=None requires Secure
  • Partitioned requires Secure, SameSite=None, and Path=/
  • __Secure- cookie names require Secure
  • __Host- cookie names require Secure, Path=/, and no Domain

If you need to parse existing Set-Cookie headers, ResponseCookie::fromSetCookieHeaderRepresentation understands Expires, SameSite, Priority, and Partitioned along with the standard attributes.

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.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
  ResponseMarshaler.builder()  
    .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.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
  ResponseMarshaler.builder()
    .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.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
  ResponseMarshaler.builder()
    .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.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
  ResponseMarshaler.builder()
    .resourceMethodHandler(...)
    .throwableHandler(...)
    .notFoundHandler(...)
    .postProcessor(postProcessor)
    .build()
).build();

References:

Zero-Copy Responses

When a Resource Method already knows the exact body it wants to write, it can return MarshaledResponse directly. This bypasses normal response marshaling and lets Soklet's standard HTTP server write some known-length bodies more efficiently.

MarshaledResponseBody.File bodies are known-length. Soklet validates the path as a readable regular file, records its size, and the standard HTTP server can write it without first loading the whole file into a byte[].

There are three file-serving layers:

APIUse it whenWhat it does
MarshaledResponse.Builder::body(Path)You already chose a file and only want that file as the response bodyAdds a raw file body. You set status, Content-Type, cache headers, and any other headers yourself. It does not evaluate validators or ranges.
MarshaledResponse::withFileYou already chose one trusted file, but want HTTP file-response semanticsBuilds a request-aware file response with validators, Last-Modified, single byte ranges, If-Range, Content-Encoding, and HEAD behavior. It does not do static-root path containment.
StaticFilesA client-provided path participates in resolving a file under a static rootPerforms root containment, path rejection, index lookup, symlink policy, MIME defaults, per-path resolvers, validators, and single byte ranges.

You can also provide a file slice, a FileChannel, or a pre-rendered ByteBuffer:

// Zero-copy responses can use a regular file path...
Path filePath = Path.of("/home/user/export.bin");
Long offset = 1024L;
Long count = Files.size(filePath) - offset;

return MarshaledResponse.withStatusCode(206)
  .headers(Map.of("Content-Type", Set.of("application/octet-stream")))
  .body(filePath, offset, count)
  .build();
// ...or an NIO FileChannel...
FileChannel fileChannel = FileChannel.open(
  Path.of("/home/user/export.bin"),
  StandardOpenOption.READ
);
Long offset = 0L;
Long count = fileChannel.size();
Boolean closeOnComplete = true;

return MarshaledResponse.withStatusCode(200)
  .body(fileChannel, offset, count, closeOnComplete)
  .headers(Map.of("Content-Type", Set.of("application/octet-stream")))
  .build();
// ...or an NIO ByteBuffer.
ByteBuffer cachedPayload = ByteBuffer.wrap(
  "{\"status\":\"ok\"}".getBytes(StandardCharsets.UTF_8)
);

return MarshaledResponse.withStatusCode(200)
  .body(cachedPayload)
  .headers(Map.of("Content-Type", Set.of("application/json")))
  .build();

For ByteBuffer bodies, the buffer's position and limit at call time define the response slice. Soklet stores a read-only slice and does not mutate the caller's buffer position. For file-channel bodies, pass false for closeOnComplete when you want to retain ownership of the channel, or true when Soklet should close it after success or failure.

For already-compressed files, use MarshaledResponse::withFile and set contentEncoding("gzip"). Soklet does not automatically negotiate precompressed file variants; if the chosen file depends on Accept-Encoding, add an appropriate Vary: Accept-Encoding header yourself. Standard HTTP's opt-in responseGzipPolicy setting applies to finalized in-memory byte-array and ByteBuffer responses, not file or streaming responses.

Use byte-array bodies when your marshaler already has finalized bytes in memory:

// Here we turn JSON into UTF-8 bytes
byte[] body = GSON.toJson(Map.of("status", "ok"))
  .getBytes(StandardCharsets.UTF_8);

return MarshaledResponse.withStatusCode(200)
  .headers(Map.of("Content-Type", Set.of("application/json;charset=UTF-8")))
  .body(body)
  .build();

Passing null to a single-argument MarshaledResponse body builder method removes any current body. This lets custom marshalers pass nullable serialized bytes directly when the logical response has no body. Multi-argument body methods require a non-null body source because their offset, count, and ownership parameters only make sense when that source is present.

MarshaledResponse::getBody returns an Optional containing a MarshaledResponseBody. Inspect the concrete descriptor when you need to distinguish byte-array, file, file-channel, or ByteBuffer bodies:

// You might need to work with a marshaled response body
// directly in test code, e.g. verifying JSON sent over the wire
// See "Testing" documentation for examples
MarshaledResponseBody responseBody =
  marshaledResponse.getBody().orElseThrow();

if (responseBody instanceof MarshaledResponseBody.Bytes bytesBody) {
  // Do something with a byte-array-backed body
  byte[] bytes = bytesBody.getBytes();
} else if (responseBody instanceof MarshaledResponseBody.File fileBody) {
  // Do something with a file-backed body
  Path file = fileBody.getPath();
  Long offset = fileBody.getOffset();
  Long count = fileBody.getCount();
}

If you only need the number of body bytes, use MarshaledResponse::getBodyLength; it works for all known-length body types.

References:

Streaming Responses

Use StreamingResponseBody when a response is produced over time and the final byte length is not known upfront. Common examples include LLM token streams, log tails, generated CSV or NDJSON exports, and long-running backend calls where you want clients to see progress before the full result exists.

Zero-copy and streaming responses solve related but different problems. Use zero-copy responses when the response is already a known-length byte sequence, especially a file or file slice. Use streaming responses when the bytes are produced incrementally and Soklet cannot know the final Content-Length before writing begins.

Streaming responses are intentionally returned through MarshaledResponse, not Response. Like file-backed or ByteBuffer bodies, a stream says "I already know what should be written to the HTTP response, and I need Soklet to use a specific write path." It bypasses normal Response body marshaling and uses stream(...) instead of body(...):

@GET("/tokens")
public MarshaledResponse tokens(TokenService tokenService) {
  return MarshaledResponse.withStatusCode(200)
    // Tell intermediaries this is plain streaming text
    .headers(Map.of(
      "Content-Type", Set.of("text/plain; charset=UTF-8"),
      "Cache-Control", Set.of("no-transform")
    ))
    .stream(StreamingResponseBody.fromWriter((output, context) -> {
      // Stop upstream work promptly if the client goes away
      try (AutoCloseable ignored = context.onCancel(tokenService::stop)) {
        tokenService.generate(token -> {
          context.throwIfCanceled();
          output.write(token.getBytes(StandardCharsets.UTF_8));
          output.flush(); // Optional latency hint
        });
      }
    }))
    .build();
}

ResponseStream is intentionally not thread-safe. Use one active writer at a time; if your application produces bytes from multiple threads, serialize those writes before calling write(...). write(byte[]) and write(ByteBuffer) copy the bytes before returning, so callers may safely reuse their arrays or buffers after the write call completes.

flush() is an advisory latency hint, not a correctness requirement. Soklet always flushes remaining bytes when the stream completes.

Soklet writes streaming HTTP responses with HTTP/1.1 chunked transfer encoding. Do not set Content-Length or Transfer-Encoding yourself on a streaming MarshaledResponse; Soklet owns those protocol headers. Streaming responses are rejected for HTTP/1.0 requests with 505 HTTP Version Not Supported and a stream termination reason of PROTOCOL_UNSUPPORTED. Streaming bodies are also rejected for bodyless status codes like 204 No Content and 304 Not Modified.

Streaming Body Sources

The preferred dynamic APIs are writer and publisher based:

StreamingResponseBody writerBody =
  StreamingResponseBody.fromWriter((output, context) -> {
    // Write bytes as your application produces them
    output.write("hello\n".getBytes(StandardCharsets.UTF_8));
  });
// Use this when you already have a JDK Flow.Publisher
StreamingResponseBody publisherBody =
  StreamingResponseBody.fromPublisher(byteBufferPublisher);

The Flow.Publisher option uses the JDK's built-in reactive-streams interfaces; no third-party dependency is involved. Soklet requests one item at a time from the publisher, copies emitted ByteBuffer contents before onNext returns, and applies its normal streaming backpressure queue. If the socket-side queue is full, onNext may block until space is available; this adapter is a simple bounded bridge, not a fully non-blocking reactive runtime. If the response is canceled before the publisher terminates, Soklet cancels the publisher subscription.

fromInputStream(...) and fromReader(...) are adapters for special cases where an existing source already exposes those JDK abstractions:

// Wrap an existing stream source without buffering the full object first
StreamingResponseBody inputStreamBody =
  StreamingResponseBody.fromInputStream(() -> blobStore.openObjectStream(key));
// Reader bodies declare the charset Soklet should use for encoding
StreamingResponseBody readerBody =
  StreamingResponseBody.withReader(
    () -> reportService.openReportReader(), 
    StandardCharsets.UTF_8
  )
  .malformedInputAction(CodingErrorAction.REPORT)
  .unmappableCharacterAction(CodingErrorAction.REPORT)
  .build();

Reader bodies require an explicit charset. Encoder error actions default to CodingErrorAction.REPORT; use the builder methods when a legacy data source needs different JDK encoder behavior. If a stream is canceled while an InputStream or Reader body is blocked in a read, Soklet closes the source so ordinary close-aware JDK implementations can unblock and terminate.

Cancelation

Every streaming producer receives a StreamingResponseContext. It exposes a CancelationToken, the originating Request, an optional deadline, and an optional idle timeout.

Streaming producers can read parsed W3C trace data through context.getRequest().getTraceContext(); see Trace Context for parsing and propagation details.

Check the token between expensive operations and register cleanup callbacks for upstream work:

@GET("/ai/tokens")
public MarshaledResponse tokens(OpenAiClient openAiClient) {
  return MarshaledResponse.withStatusCode(200)
    // Keep the response simple: token bytes, no transformation
    .headers(Map.of(
      "Content-Type", Set.of("text/plain; charset=UTF-8"),
      "Cache-Control", Set.of("no-transform")
    ))
    .stream(StreamingResponseBody.fromWriter((output, context) -> {
      // The request is still available inside the producer
      Request request = context.getRequest();
      String requestId = String.valueOf(request.getId());
      OpenAiStream upstream = openAiClient.startStreaming(request);

      // Closing the upstream stream releases work 
      // as soon as the client disconnects
      try (AutoCloseable ignored = context.onCancel(upstream::close)) {
        for (String token : upstream) {
          context.throwIfCanceled();
          log.debug("Streaming token for request {}", requestId);
          output.write(token.getBytes(StandardCharsets.UTF_8));
        }
      }
    }))
    .build();
}

Soklet cancels the token when the client disconnects, the server stops, an HTTP/1.0 request attempts to use streaming, a streaming timeout fires, the producer fails, the simulator limit is exceeded, or producer code explicitly aborts by throwing a StreamingResponseCanceledException. Use StreamTerminationReason.APPLICATION_CANCELED when producer code intentionally aborts the stream. Cancelation callbacks run synchronously on the thread that performs cancelation, so keep them quick and non-blocking; dispatch slower cleanup to your own executor. The public reason enum is StreamTerminationReason, which is also used by SSE and MCP stream lifecycle callbacks.

if (userAbortedRequest) {
  // Use an application cancelation reason for intentional aborts
  throw new StreamingResponseCanceledException(
    StreamTerminationReason.APPLICATION_CANCELED);
}

References:

Copying Responses

When you want to adjust an existing Response (for example, adding a header during post-processing), use Response::copy to get a mutable copier, change the fields you need, then call Response.Copier::finish to build the new instance.

This "copy-to-change" workflow is necessary because Response objects are effectively immutable.

Response updatedResponse = response.copy()
  // 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-Powered-By", Set.of("Soklet"));
  })
  .finish();

The copy is shallow for performance (for example, the response body object reference is reused), so treat the original as immutable.

If you're working with a MarshaledResponse, it also provides MarshaledResponse::copy for the same purpose (tweaking headers, status, or body) before writing the final response.

Advanced Response Marshaling

Experts Only

You can normally use the out-of-the-box implementation provided by ResponseMarshaler::builder 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.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
    ResponseMarshaler.builder()
      .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. For the standard HTTP server, the configured limit applies to the whole received HTTP request, including request line, headers, transfer framing, and body bytes. 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. If the transport cannot parse enough data to identify a request target before the limit is exceeded, Soklet may close the connection instead of invoking this marshaling path.

Soklet is designed to power systems that exchange small "transactional" payloads. It can serve known-length file-backed responses, but request uploads are still buffered and bounded by the configured request-size limit; Soklet is not intended for handling multipart uploads at scale, buffering uploads to disk, streaming request bodies, 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.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
  ResponseMarshaler.builder()
    .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:

503 Service Unavailable

Soklet returns a 503 Service Unavailable response when it must reject a request due to capacity limits, such as when the standard HTTP, SSE, or MCP transport exceeds its connection limit. See the Server Configuration documentation for how to adjust these limits.

The default 503 response includes a plain-text body and a Connection: close header to signal clients to terminate the connection and back off:

HTTP/1.1 503 Service Unavailable
Content-Type: text/plain; charset=UTF-8
Connection: close

HTTP 503: Service Unavailable

If you'd like to customize this response (for example, return JSON and add Retry-After), provide a ServiceUnavailableHandler:

ServiceUnavailableHandler serviceUnavailableHandler = (
  @NonNull Request request,
  @Nullable ResourceMethod resourceMethod
) -> {
  int statusCode = 503;

  // Turn response body into JSON bytes with Gson.
  // Note: real systems should localize this message
  String message = "Try again shortly.";
  byte[] body = GSON.toJson(Map.of("message", message))
    .getBytes(StandardCharsets.UTF_8);

  return MarshaledResponse.withStatusCode(statusCode)
    .headers(Map.of(
      "Content-Type", Set.of("application/json;charset=UTF-8"),
      "Connection", Set.of("close"),
      "Retry-After", Set.of("10")
    ))
    .body(body)
    .build();
};

SokletConfig config = SokletConfig.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
  ResponseMarshaler.builder()
    .serviceUnavailableHandler(serviceUnavailableHandler)
    .build()
).build();

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.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
  ResponseMarshaler.builder()
    .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.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
  ResponseMarshaler.builder()
    .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 for a known-length response 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

For streaming responses, the default HEAD behavior removes the stream and omits Content-Length, because the GET length is not known upfront.

Here is an example of a custom implementation (which is essentially identical to Soklet's default implementation for known-length responses):

HeadHandler headHandler = (
  @NonNull Request request,
  @NonNull MarshaledResponse getMethodMarshaledResponse
) -> {
  // Soklet automatically marshals the GET response for you
  Long getResponseLength = getMethodMarshaledResponse.getBodyLength();

  // MarshaledResponse is immutable so we make a mutable copy
  // of the GET response to construct our HEAD response.
  MarshaledResponse.Copier responseCopier = getMethodMarshaledResponse.copy()
    // Ensure no response body bytes are written
    .withoutBody()
    .withoutStream()
    .cookies(getMethodMarshaledResponse.getCookies());

  if (getMethodMarshaledResponse.isStreaming()) {
    responseCopier.headers((mutableHeaders) -> {
      mutableHeaders.remove("Content-Length");
    });
  } else {
    responseCopier.headers((mutableHeaders) -> {
      // For convenience, Soklet provides a mutable representation
      // of the response headers when using the copy builder.
      // Here, we specify our own Content-Length value using
      // the length of the GET response body
      mutableHeaders.put("Content-Length", Set.of(
        String.valueOf(getResponseLength)
      ));
    });
  }

  return responseCopier.finish();
};

// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
  ResponseMarshaler.builder()
    .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.withHttpServer(
  HttpServer.fromPort(8080)
).responseMarshaler(
  ResponseMarshaler.builder()
    .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.withHttpServer(
  HttpServer.fromPort(8080)
// Bring your own totally-custom response marshaler
).responseMarshaler(new ResponseMarshaler() {
  @NonNull
  @Override
  public MarshaledResponse forResourceMethod(
    @NonNull Request request,
    @NonNull Response response,
    @NonNull ResourceMethod resourceMethod
  ) {
    // Application response path
  }

  @NonNull
  @Override
  public MarshaledResponse forThrowable(
    @NonNull Request request,
    @NonNull Throwable throwable,
    @Nullable ResourceMethod resourceMethod
  ) {
    // Exception response 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