{"id":41790556,"url":"https://github.com/soklet/soklet","last_synced_at":"2026-03-08T00:09:45.663Z","repository":{"id":30782436,"uuid":"34339303","full_name":"soklet/soklet","owner":"soklet","description":"Soklet is a zero-dependency Java HTTP/1.1 and Server-Sent Event server, well-suited for building RESTful APIs and tool-backed agentic systems.","archived":false,"fork":false,"pushed_at":"2026-02-07T15:58:55.000Z","size":2846,"stargazers_count":22,"open_issues_count":5,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-02-08T00:23:58.432Z","etag":null,"topics":["java","no-dependencies","server","server-sent-events","soklet"],"latest_commit_sha":null,"homepage":"https://www.soklet.com","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"aliajahar90/location","license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soklet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2015-04-21T16:38:33.000Z","updated_at":"2026-02-05T18:45:20.000Z","dependencies_parsed_at":"2023-10-12T10:58:37.522Z","dependency_job_id":"5f1ef299-c38b-4540-bfd4-efafe79573f8","html_url":"https://github.com/soklet/soklet","commit_stats":null,"previous_names":[],"tags_count":31,"template":false,"template_full_name":null,"purl":"pkg:github/soklet/soklet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soklet%2Fsoklet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soklet%2Fsoklet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soklet%2Fsoklet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soklet%2Fsoklet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soklet","download_url":"https://codeload.github.com/soklet/soklet/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soklet%2Fsoklet/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30238255,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T23:52:25.683Z","status":"ssl_error","status_checked_at":"2026-03-07T23:52:25.373Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","no-dependencies","server","server-sent-events","soklet"],"created_at":"2026-01-25T05:00:28.991Z","updated_at":"2026-03-08T00:09:45.651Z","avatar_url":"https://github.com/soklet.png","language":"Java","funding_links":[],"categories":["网络编程"],"sub_categories":["Spring Cloud框架"],"readme":"\u003ca href=\"https://www.soklet.com\"\u003e\n    \u003cpicture\u003e\n        \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://cdn.soklet.com/soklet-gh-logo-dark-v2.png\"\u003e\n        \u003cimg alt=\"Soklet\" src=\"https://cdn.soklet.com/soklet-gh-logo-light-v2.png\" width=\"300\" height=\"101\"\u003e\n    \u003c/picture\u003e\n\u003c/a\u003e\n\n### What Is It?\n\nA small [HTTP/1.1 server](https://github.com/ebarlas/microhttp) and route handler for Java, well-suited for building RESTful APIs and broadcasting [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events).\u003cbr/\u003e\u003cbr/\u003e\nZero dependencies.  Dependency Injection friendly.\u003cbr/\u003e\nOptionally powered by [JEP 444: Virtual Threads](https://openjdk.org/jeps/444).\n\nSoklet codes like a library, not a framework.\n\n**Note: this README provides a high-level overview of Soklet.**\u003cbr/\u003e\n**For details, please refer to the official documentation at [https://www.soklet.com](https://www.soklet.com).**\n\n### Why?\n\nThe Java web ecosystem is missing a server solution that is dependency-free but offers support for virtual threads, hooks for dependency injection, and annotation-based request handling. Soklet aims to fill this void.\n\nSoklet provides the plumbing to build \"transactional\" REST APIs that exchange small amounts of data with clients.\nIt is well-suited for building tool-backed agentic systems that stream results via SSE.\nIt does not make technology choices on your behalf (but [an example of how to build a full-featured API is available](https://www.soklet.com/docs/toystore-app)). It does not natively support [Reactive Programming](https://en.wikipedia.org/wiki/Reactive_programming) or similar methodologies.  It _does_ give you the foundation to build your system, your way.\n\nSoklet is [commercially-friendly Open Source Software](https://www.soklet.com/docs/licensing), proudly powering production systems since 2015.\n\n### Design Goals\n\n* Main focus: routing HTTP/1.1 requests to Java methods\n* Near-instant startup\n* Zero dependencies\n* Immutability/thread-safety\n* Small, comprehensible codebase\n* Support for automated unit and integration testing\n* Emphasis on configurability\n* Thorough, high-quality documentation\n* Best-in-class support for [Server-Sent Events](https://www.soklet.com/docs/server-sent-events)\n* [Servlet Integration](https://www.soklet.com/docs/servlet-integration) for legacy code\n\n### Design Non-Goals\n\n* SSL/TLS (your load balancer should provide TLS termination)\n* Traditional HTTP streaming\n* WebSockets\n* Dictate which technologies to use (Guice vs. Dagger, Gson vs. Jackson, etc.)\n* \"Batteries included\" authentication and authorization\n\n### Do Zero-Dependency Libraries Interest You?\n\nSimilarly-flavored commercially-friendly OSS libraries are available.\n\n* [Pyranid](https://www.pyranid.com) - makes working with JDBC pleasant\n* [Lokalized](https://www.lokalized.com) - natural-sounding translations (i18n) via expression language\n\n### License\n\n[Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)\n\n### Installation\n\nSoklet is a single JAR, available on Maven Central.\n\nJDK 17+ is required (or JDK 21+ for [Server-Sent Events](https://www.soklet.com/docs/server-sent-events)).\n\n#### Maven\n\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003ecom.soklet\u003c/groupId\u003e\n  \u003cartifactId\u003esoklet\u003c/artifactId\u003e\n  \u003cversion\u003e2.0.2\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n#### Gradle\n\n```groovy\nrepositories {\n  mavenCentral()\n}\n\ndependencies {\n  implementation 'com.soklet:soklet:2.0.2'\n}\n```\n\n#### Direct Download\n\nIf you don't use Maven or Gradle, you can drop [soklet-2.0.2.jar](https://repo1.maven.org/maven2/com/soklet/soklet/2.0.2/soklet-2.0.2.jar) directly into your project.  No other dependencies are required.\n\n### Code Sample\n\nHere we demonstrate building and running a single-file Soklet application with nothing but the [soklet-2.0.2.jar](https://repo1.maven.org/maven2/com/soklet/soklet/2.0.2/soklet-2.0.2.jar) and the JDK.  There are no other libraries or frameworks, no Servlet container, no Maven or Gradle build process - no special setup is required.\n\nSoklet systems can be structurally as simple as a \"hello world\" app.\n\nWhile a real production system will have more moving parts, this demonstrates that you _can_ build server software without ceremony or dependencies.\n\n```java\npackage com.soklet.example;\n\npublic class App {\n  // Canonical example\n  @GET(\"/\")\n  public String index() {\n    return \"Hello, world!\";\n  }\n  \n  // Echoes back the path parameter, which must be a LocalDate\n  @GET(\"/echo/{date}\")\n  public LocalDate echo(@PathParameter LocalDate date) {\n    return date;\n  }\n\n  // Formats request body locale for display and customizes the response.\n  // Example: fr-CA ⇒ francês (Canadá)\n  @POST(\"/language\")\n  public Response languageFor(@RequestBody Locale locale) {\n    Locale systemLocale = Locale.forLanguageTag(\"pt-BR\");\n    String contentLanguage = systemLocale.toLanguageTag();\n\n    return Response.withStatusCode(200)\n      .body(locale.getDisplayName(systemLocale))\n      .headers(Map.of(\"Content-Language\", Set.of(contentLanguage)))\n      .cookies(Set.of(\n        ResponseCookie.withName(\"lastRequest\")\n          .value(Instant.now().toString())\n          .httpOnly(true)\n          .secure(true)\n          .maxAge(Duration.ofMinutes(5))\n          .sameSite(SameSite.LAX)\n          .build()\n      ))        \n      .build();\n  }\n\n  // Start the server and listen on :8080\n  public static void main(String[] args) throws Exception {\n    // Use out-of-the-box defaults\n    SokletConfig config = SokletConfig.withServer(\n      Server.fromPort(8080)\n    ).build();\n\n    try (Soklet soklet = Soklet.fromConfig(config)) {\n      soklet.start();\n      System.out.println(\"Soklet started, press [enter] to exit\");\n      soklet.awaitShutdown(ShutdownTrigger.ENTER_KEY);\n    }\n  }\n}\n```\n\nHere we use raw `javac` to build and `java` to run.\n\nThis example requires JDK 17+ to be installed on your machine ([or see this example of using Docker for Soklet apps](https://github.com/soklet/barebones-app?tab=readme-ov-file#building-and-running-with-docker)).  If you need a JDK, Amazon provides [Corretto](https://aws.amazon.com/corretto/) - a free-to-use-commercially, production-ready distribution of [OpenJDK](https://openjdk.org/) that includes long-term support.\n\n#### Build\n\n```shell\njavac -parameters -cp soklet-2.0.2.jar -processor com.soklet.SokletProcessor -d build src/com/soklet/example/App.java \n```\n\n#### Run\n\n```shell\njava -cp soklet-2.0.2.jar:build com/soklet/example/App\n```\n\n#### Test\n\n```shell\n# Hello, world\n% curl -i 'http://localhost:8080/'\nHTTP/1.1 200 OK\nContent-Length: 13\nContent-Type: text/plain; charset=UTF-8\nDate: Sun, 21 Mar 2024 16:19:01 GMT\n\nHello, world!\n```\n\n```shell\n# Acceptable path parameter\n% curl -i 'http://localhost:8080/echo/2024-12-31' \nHTTP/1.1 200 OK\nContent-Length: 10\nContent-Type: text/plain; charset=UTF-8\nDate: Sun, 21 Mar 2024 16:19:01 GMT\n\n2024-12-31\n```\n\n```shell\n# Illegal path parameter\n% curl -i 'http://localhost:8080/echo/abc'\nHTTP/1.1 400 Bad Request\nContent-Length: 21\nContent-Type: text/plain; charset=UTF-8\nDate: Sun, 21 Mar 2024 16:19:01 GMT\n\nHTTP 400: Bad Request\n```\n\n```shell\n# Language request body\n% curl -i -X POST 'http://localhost:8080/language' -d 'fr-CA'\nHTTP/1.1 200 OK\nContent-Language: pt-BR\nContent-Length: 18\nContent-Type: text/plain; charset=UTF-8\nDate: Sun, 21 Mar 2024 16:19:01 GMT\nSet-Cookie: lastRequest=2024-04-21T16:19:01.115336Z; Max-Age=300; Secure; HttpOnly; SameSite=Lax\n\nfrancês (Canadá)\n```\n\n### Building Real-World Apps\n\nOf course, real-world apps have more moving parts than a \"hello world\" example.\n\n[The Toy Store App](https://www.soklet.com/docs/toystore-app) showcases how you might build a robust production system with Soklet.\n\nFeature highlights include:\n\n* Authentication and role-based authorization\n* Basic CRUD operations\n* Dependency injection via [Google Guice](https://github.com/google/guice)\n* Relational database integration via [Pyranid](https://www.pyranid.com)\n* Context-awareness via [ScopedValue (JEP 481)](https://openjdk.org/jeps/481)\n* Internationalization via the JDK and [Lokalized](https://www.lokalized.com)\n* JSON requests/responses via [Gson](https://github.com/google/gson)\n* Logging via [SLF4J](https://slf4j.org/) / [Logback](https://logback.qos.ch/)\n* Metrics collection via [`MetricsCollector`](https://javadoc.soklet.com/com/soklet/MetricsCollector.html)\n* Automated unit and integration tests via [JUnit](https://junit.org)\n* Ability to run in [Docker](https://www.docker.com/)\n\n### What Else Does It Do?\n\n#### Request Handling\n\nSoklet maps HTTP requests to plain Java methods known as Resource Methods\n([`ResourceMethod`](https://javadoc.soklet.com/com/soklet/ResourceMethod.html)).\nAnnotate them with [`@GET`](https://javadoc.soklet.com/com/soklet/annotation/GET.html),\n[`@POST`](https://javadoc.soklet.com/com/soklet/annotation/POST.html),\n[`@PUT`](https://javadoc.soklet.com/com/soklet/annotation/PUT.html),\n[`@PATCH`](https://javadoc.soklet.com/com/soklet/annotation/PATCH.html),\n[`@DELETE`](https://javadoc.soklet.com/com/soklet/annotation/DELETE.html),\n[`@HEAD`](https://javadoc.soklet.com/com/soklet/annotation/HEAD.html),\n[`@OPTIONS`](https://javadoc.soklet.com/com/soklet/annotation/OPTIONS.html), or\n[`@ServerSentEventSource`](https://javadoc.soklet.com/com/soklet/annotation/ServerSentEventSource.html) for SSE.\nSoklet discovers them at compile time via the\n[`SokletProcessor`](https://javadoc.soklet.com/com/soklet/SokletProcessor.html) annotation processor, avoiding\nclasspath scans at startup. See the [Request Handling](https://www.soklet.com/docs/request-handling) docs for details.\n\n#### Access To Request Data\n\nResource Methods ([`ResourceMethod`](https://javadoc.soklet.com/com/soklet/ResourceMethod.html)) can accept a\n[`Request`](https://javadoc.soklet.com/com/soklet/Request.html) parameter and inspect\n[`HttpMethod`](https://javadoc.soklet.com/com/soklet/HttpMethod.html) values.\n\n```java\n@GET(\"/example\")\npublic void example(Request request /* param name is arbitrary */) {\n  // Here, it would be HttpMethod.GET\n  HttpMethod httpMethod = request.getHttpMethod();\n  // Just the path, e.g. \"/example\"\n  String path = request.getPath(); \n  // The raw path and query, e.g. \"/example?test=123\"\n  String rawPathAndQuery = request.getRawPathAndQuery();\n  // Request body as bytes, if available\n  Optional\u003cbyte[]\u003e body = request.getBody();\n  // Request body marshaled to a string, if available.\n  // Charset defined in \"Content-Type\" header is used to marshal.\n  // If not specified, UTF-8 is assumed\n  Optional\u003cString\u003e bodyAsString = request.getBodyAsString();\n  // Query parameter values by name\n  Map\u003cString, Set\u003cString\u003e\u003e queryParameters = request.getQueryParameters();\n  // Shorthand for plucking the first query param value by name\n  Optional\u003cString\u003e queryParameter = request.getQueryParameter(\"test\");\n  // Header values by name (names are case-insensitive)\n  Map\u003cString, Set\u003cString\u003e\u003e headers = request.getHeaders();\n  // Shorthand for plucking the first header value by name (case-insensitive)\n  Optional\u003cString\u003e header = request.getHeader(\"Accept-Language\");\n  // Request cookies by name (names are case-insensitive)\n  Map\u003cString, Set\u003cString\u003e\u003e cookies = request.getCookies();\n  // Shorthand for plucking the first cookie value by name (case-insensitive)\n  Optional\u003cString\u003e cookie = request.getCookie(\"cookie-name\");\n  // Form parameters by name (application/x-www-form-urlencoded)\n  Map\u003cString, Set\u003cString\u003e\u003e fps = request.getFormParameters();\n  // Shorthand for plucking the first form parameter value by name\n  Optional\u003cString\u003e fp = request.getFormParameter(\"fp-name\");  \n  // Is this a multipart request?\n  boolean multipart = request.isMultipart();\n  // Multipart fields by name\n  Map\u003cString, Set\u003cMultipartField\u003e\u003e mpfs = request.getMultipartFields();\n  // Shorthand for plucking the first multipart field by name\n  Optional\u003cMultipartField\u003e mpf = request.getMultipartField(\"file-input\");  \n  // CORS information, if available\n  Optional\u003cCors\u003e cors = request.getCors();\n  // Ordered locales via Accept-Language parsing\n  List\u003cLocale\u003e locales = request.getLocales();\n  // Charset as specified by \"Content-Type\" header, if available\n  Optional\u003cCharset\u003e charset = request.getCharset();\n  // Content type component of \"Content-Type\" header, if available\n  Optional\u003cString\u003e contentType = request.getContentType();\n}\n```\n\n#### Value Conversions\n\nSoklet converts textual request inputs to Java types using a\n[`ValueConverterRegistry`](https://javadoc.soklet.com/com/soklet/converter/ValueConverterRegistry.html) populated with\n[`ValueConverter\u003cF,T\u003e`](https://javadoc.soklet.com/com/soklet/converter/ValueConverter.html).\nConversions are applied to parameters annotated with\n[`@QueryParameter`](https://javadoc.soklet.com/com/soklet/annotation/QueryParameter.html),\n[`@PathParameter`](https://javadoc.soklet.com/com/soklet/annotation/PathParameter.html),\n[`@RequestHeader`](https://javadoc.soklet.com/com/soklet/annotation/RequestHeader.html),\n[`@RequestCookie`](https://javadoc.soklet.com/com/soklet/annotation/RequestCookie.html),\n[`@FormParameter`](https://javadoc.soklet.com/com/soklet/annotation/FormParameter.html), and\n[`@Multipart`](https://javadoc.soklet.com/com/soklet/annotation/Multipart.html).\nSupply your own registry (or additional converters) via\n[`SokletConfig`](https://javadoc.soklet.com/com/soklet/SokletConfig.html) to support custom types.\n\n#### Request Body Parsing\n\nConfigure a [`RequestBodyMarshaler`](https://javadoc.soklet.com/com/soklet/RequestBodyMarshaler.html) however you like - here we accept JSON:\n\n```java\nSokletConfig config = SokletConfig.withServer(\n  Server.fromPort(8080)\n).requestBodyMarshaler(new RequestBodyMarshaler() {\n  // This example uses Google's GSON\n  static final Gson GSON = new Gson();\n\n  @NonNull\n  @Override\n  public Optional\u003cObject\u003e marshalRequestBody(\n    @NonNull Request request,\n    @NonNull ResourceMethod resourceMethod,\n    @NonNull Parameter parameter,\n    @NonNull Type requestBodyType\n  ) {\n    // Let GSON turn the request body into an instance\n    // of the specified type.\n    //\n    // Note that this method has access to all runtime information \n    // about the request, which provides the opportunity to, for example,\n    // examine annotations on the method/parameter which might\n    // inform custom marshaling strategies.\n    return Optional.of(GSON.fromJson(\n      request.getBodyAsString().orElseThrow(),\n      requestBodyType\n    ));\n  }\n}).build();\n```\n\nThen, apply:\n\n```java\npublic record Employee (\n  UUID id,\n  String name\n) {}\n\n// Accepts a JSON-formatted Record type as input\n@POST(\"/employees\")\npublic void createEmployee(@RequestBody Employee employee) {\n  System.out.printf(\"TODO: create %s\\n\", employee.name());\n}\n```\n\n#### Response Writing\n\nTo control how response data is surfaced to clients (e.g. JSON), provide handler functions\n([`ResourceMethodHandler`](https://javadoc.soklet.com/com/soklet/ResponseMarshaler.ResourceMethodHandler.html) and\n[`ThrowableHandler`](https://javadoc.soklet.com/com/soklet/ResponseMarshaler.ThrowableHandler.html)) to Soklet as shown below.\n\nAlternatively, you can provide your own implementation of [`ResponseMarshaler`](https://javadoc.soklet.com/com/soklet/ResponseMarshaler.html) for full control.\n\n```java\n// Let's use Gson to write response body data\n// See https://github.com/google/gson\nfinal Gson GSON = new Gson();\n\n// The request was matched to a Resource Method and executed non-exceptionally\nResourceMethodHandler resourceMethodHandler = (\n  @NonNull Request request,\n  @NonNull Response response,\n  @NonNull ResourceMethod resourceMethod\t\t\n) -\u003e {\n  // Turn response body into JSON bytes with Gson\n  Object bodyObject = response.getBody().orElse(null);\n  byte[] body = bodyObject == null\n    ? null\n    : GSON.toJson(bodyObject).getBytes(StandardCharsets.UTF_8);\n\n  // To be a good citizen, set the Content-Type header\n  Map\u003cString, Set\u003cString\u003e\u003e headers = new HashMap\u003c\u003e(response.getHeaders());\n  headers.put(\"Content-Type\", Set.of(\"application/json;charset=UTF-8\"));\n\n  // Tell Soklet: \"OK - here is the final response data to send\"\n  return MarshaledResponse.withResponse(response)\n    .headers(headers)\n    .body(body)\n    .build();\n};\n\n// Function to create responses for exceptions that bubble out\nThrowableHandler throwableHandler = (\n  @NonNull Request request,\n  @NonNull Throwable throwable,\n  @Nullable ResourceMethod resourceMethod\n) -\u003e {\n  // Keep track of what to write to the response\n  String message;\n  int statusCode;\n\n  // Examine the exception that bubbled out and determine what \n  // the HTTP status and a user-facing message should be.\n  // Note: real systems should localize these messages\n  switch (throwable) {\n    // Soklet throws this exception, a specific subclass of BadRequestException\n    case IllegalQueryParameterException e -\u003e {\n      message = String.format(\"Illegal value '%s' for parameter '%s'\",\n        e.getQueryParameterValue().orElse(\"[not provided]\"),\n        e.getQueryParameterName());\n      statusCode = 400;\n    }\n    \n    // Generically handle other BadRequestExceptions\n    case BadRequestException ignored -\u003e {\n      message = \"Your request was improperly formatted.\";\n      statusCode = 400;\n    }\n    \n    // Something else?  Fall back to a 500\n    default -\u003e {\n      message = \"An unexpected error occurred.\";\n      statusCode = 500;\n    }\n  }\n\n  // Turn response body into JSON bytes with Gson.\n  // Note: real systems should expose richer error constructs\n  // than an object with a single message field\n  byte[] body = GSON.toJson(Map.of(\"message\", message))\n    .getBytes(StandardCharsets.UTF_8);\n\n  // Specify our headers\n  Map\u003cString, Set\u003cString\u003e\u003e headers = new HashMap\u003c\u003e();\n  headers.put(\"Content-Type\", Set.of(\"application/json;charset=UTF-8\"));\n\n  return MarshaledResponse.withStatusCode(statusCode)\n    .headers(headers)\n    .body(body)\n    .build();\n};\n\n// Supply our custom handlers to the standard response marshaler\nSokletConfig config = SokletConfig.withServer(\n  Server.fromPort(8080)\n).responseMarshaler(ResponseMarshaler.builder()\n  .resourceMethod(resourceMethodHandler)\n  .throwable(throwableHandler)\n  .build()\n).build();\n```\n\nAlready know exactly what bytes you want to send over the wire? Use [`MarshaledResponse`](https://javadoc.soklet.com/com/soklet/MarshaledResponse.html) to skip additional processing.\n\n```java\n@GET(\"/example-image.png\")\npublic MarshaledResponse exampleImage() throws IOException {\n  Path imageFile = Path.of(\"/home/user/test.png\");\n  byte[] image = Files.readAllBytes(imageFile);\n  \n  // Serve \"final\" bytes over the wire\n  return MarshaledResponse.withStatusCode(200)\n    .headers(Map.of(\n      \"Content-Type\", Set.of(\"image/png\"),\n      \"Content-Length\", Set.of(String.valueOf(image.length))\n    ))\n    .body(image)\n    .build();\n}\n```\n\nRedirects (via [`Response`](https://javadoc.soklet.com/com/soklet/Response.html)):\n\n```java\n@GET(\"/example-redirect\")\npublic Response exampleRedirect() {\n  // Response has a convenience builder for performing redirects.\n  // You could alternatively do this \"by hand\" by setting HTTP status\n  // and headers appropriately.\n  return Response.withRedirect(\n    RedirectType.HTTP_307_TEMPORARY_REDIRECT, \"/other-url\"\n  ).build();\n}\n```\n\n#### Server Configuration\n\nSoklet ships with an embedded HTTP/1.1 [`Server`](https://javadoc.soklet.com/com/soklet/Server.html) and (for SSE) a\ndedicated [`ServerSentEventServer`](https://javadoc.soklet.com/com/soklet/ServerSentEventServer.html).\nBoth builders let you configure host, timeouts, handler concurrency/queueing, request size limits, and connection caps; you\ncan also plug in custom [`IdGenerator`](https://javadoc.soklet.com/com/soklet/IdGenerator.html) and\n[`MultipartParser`](https://javadoc.soklet.com/com/soklet/MultipartParser.html) instances.\nProvide the configured servers via [`SokletConfig`](https://javadoc.soklet.com/com/soklet/SokletConfig.html) and see the\n[Server Configuration](https://www.soklet.com/docs/server-configuration) docs for the full option matrix.\n\n#### Server-Sent Events (SSE)\n\nSSE endpoints are declared with [`@ServerSentEventSource`](https://javadoc.soklet.com/com/soklet/annotation/ServerSentEventSource.html) and return a\n[`HandshakeResult`](https://javadoc.soklet.com/com/soklet/HandshakeResult.html), served from a dedicated\n[`ServerSentEventServer`](https://javadoc.soklet.com/com/soklet/ServerSentEventServer.html) port (separate from your standard HTTP server port).\n\n```java\npublic record ChatMessage(String message) {}\n\npublic class ChatResource {\n  @ServerSentEventSource(\"/chat\")\n  public HandshakeResult chat() {\n    return HandshakeResult.Accepted.builder()\n      .clientInitializer(unicaster -\u003e {\n        unicaster.unicastEvent(ServerSentEvent.withEvent(\"hello\")\n          .data(\"welcome\")\n          .build());\n      })\n      .build();\n  }\n\n  @POST(\"/chat\")\n  public void postMessage(@RequestBody ChatMessage message,\n                          @NonNull ServerSentEventServer sseServer) {\n    ServerSentEventBroadcaster broadcaster = sseServer\n      .acquireBroadcaster(ResourcePath.fromPath(\"/chat\"))\n      .orElseThrow();\n\n    broadcaster.broadcastEvent(ServerSentEvent.withEvent(\"message\")\n      .data(message.message())\n      .build());\n  }\n}\n```\n\nWire up both servers:\n\n```java\nSokletConfig config = SokletConfig.withServer(\n  Server.fromPort(8080)\n).serverSentEventServer(\n  ServerSentEventServer.fromPort(8081)\n).resourceMethodResolver(\n  ResourceMethodResolver.fromClasses(Set.of(ChatResource.class))\n).build();\n```\n\nSSE test via the [`Simulator`](https://javadoc.soklet.com/com/soklet/Simulator.html)\n(see [`ServerSentEventRequestResult`](https://javadoc.soklet.com/com/soklet/ServerSentEventRequestResult.html)):\n\n```java\nimport org.junit.Assert;\nimport org.junit.Test;\n\n@Test\npublic void sseTest() {\n  SokletConfig config = SokletConfig.forSimulatorTesting()\n    .serverSentEventServer(ServerSentEventServer.fromPort(0))\n    .resourceMethodResolver(ResourceMethodResolver.fromClasses(Set.of(ChatResource.class)))\n    .build();\n\n  List\u003cServerSentEvent\u003e events = new ArrayList\u003c\u003e();\n\n  Soklet.runSimulator(config, simulator -\u003e {\n    Request request = Request.fromPath(HttpMethod.GET, \"/chat\");\n    ServerSentEventRequestResult result = simulator.performServerSentEventRequest(request);\n\n    if (result instanceof ServerSentEventRequestResult.HandshakeAccepted accepted) {\n      accepted.registerEventConsumer(events::add);\n\n      ServerSentEventBroadcaster broadcaster = config.getServerSentEventServer().orElseThrow()\n        .acquireBroadcaster(ResourcePath.fromPath(\"/chat\")).orElseThrow();\n      broadcaster.broadcastEvent(ServerSentEvent.withEvent(\"message\")\n        .data(\"hello\")\n        .build());\n    } else {\n      throw new IllegalStateException(\"SSE handshake failed: \" + result);\n    }\n  });\n\n  Assert.assertEquals(\"hello\", events.get(0).getData().orElse(null));\n}\n```\n\n#### Form Handling\n\nFrontend:\n\n```html\n\u003cform \n  enctype=\"application/x-www-form-urlencoded\"\n  action=\"https://example.soklet.com/form?id=123\"\n  method=\"POST\"\u003e\n  \u003c!-- User can type whatever text they like --\u003e\n  \u003cinput type=\"number\" name=\"numericValue\" /\u003e\n  \u003c!-- Multiple values for the same name are supported --\u003e\n  \u003cinput type=\"hidden\" name=\"multi\" value=\"1\" /\u003e\n  \u003cinput type=\"hidden\" name=\"multi\" value=\"2\" /\u003e\n  \u003c!-- Names with special characters can be remapped --\u003e\n  \u003ctextarea name=\"long-text\"\u003e\u003c/textarea\u003e\n  \u003c!-- Note: browsers send \"on\" string to indicate \"checked\" --\u003e\n  \u003cinput type=\"checkbox\" name=\"enabled\"/\u003e\n  \u003cinput type=\"submit\"/\u003e\n\u003c/form\u003e\n```\n\nBackend:\n\nBackend parameters can use [`@QueryParameter`](https://javadoc.soklet.com/com/soklet/annotation/QueryParameter.html) and\n[`@FormParameter`](https://javadoc.soklet.com/com/soklet/annotation/FormParameter.html).\n\n```java\n@POST(\"/form\")\npublic String form(\n  @QueryParameter Long id,\n  @FormParameter Integer numericValue,\n  @FormParameter(optional=true) List\u003cString\u003e multi,\n  @FormParameter(name=\"long-text\") String longText,\n  @FormParameter String enabled\n) {\n  // Echo back the inputs\n  return List.of(id, numericValue, multi, longText, enabled).stream()\n    .map(Object::toString)\n    .collect(Collectors.joining(\"\\n\"));\n}\n```\n\nTest:\n\n```shell\n% curl -i -X POST 'https://example.soklet.com/form?id=123' \\\n   -H 'Content-Type: application/x-www-form-urlencoded' \\\n   -d 'numericValue=456\u0026multi=1\u0026multi=2\u0026long-text=long%20multiline%20text\u0026enabled=on'\nHTTP/1.1 200 OK\nContent-Length: 37\nContent-Type: text/plain; charset=UTF-8\nDate: Sun, 21 Mar 2024 16:19:01 GMT\n\n123\n456\n[1, 2]\nlong multiline text\non\n```\n\n#### Multipart Handling\n\nFrontend:\n\n```html\n\u003cform \n  enctype=\"multipart/form-data\"\n  action=\"https://example.soklet.com/multipart?id=123\"\n  method=\"POST\"\u003e\n  \u003c!-- User can type whatever text they like --\u003e\n  \u003cinput type=\"text\" name=\"freeform\" /\u003e\n  \u003c!-- Multiple values for the same name are supported --\u003e\n  \u003cinput type=\"hidden\" name=\"multi\" value=\"1\" /\u003e\n  \u003cinput type=\"hidden\" name=\"multi\" value=\"2\" /\u003e\n  \u003c!-- Prompt user to upload a file --\u003e\n  \u003cp\u003e\n    Please attach your document: \u003cinput name=\"doc\" type=\"file\" /\u003e\n  \u003c/p\u003e\n  \u003c!-- Multiple file uploads are supported --\u003e\n  \u003cp\u003e\n    Supplement 1: \u003cinput name=\"extra\" type=\"file\" /\u003e\n    Supplement 2: \u003cinput name=\"extra\" type=\"file\" /\u003e\n  \u003c/p\u003e  \n  \u003c!-- An optional file --\u003e\n  \u003cp\u003e\n    Optionally, attach a photo: \u003cinput name=\"photo\" type=\"file\" /\u003e\n  \u003c/p\u003e  \n  \u003cinput type=\"submit\" value=\"Upload\" /\u003e\n\u003c/form\u003e\n```\n\nBackend:\n\nBackend parameters can use [`@Multipart`](https://javadoc.soklet.com/com/soklet/annotation/Multipart.html) and\n[`MultipartField`](https://javadoc.soklet.com/com/soklet/MultipartField.html).\n\n```java\n@POST(\"/multipart\")\npublic Response multipart(\n  @QueryParameter Long id,\n  // Multipart fields work like other Soklet params\n  // with support for Optional\u003cT\u003e, List\u003cT\u003e, custom names, ...\n  @Multipart(optional=true) String freeform,\n  @Multipart(name=\"multi\") List\u003cInteger\u003e numbers,\n  // The MultipartField type allows access to additional data,\n  // like filename and content type (if available).\n  // The @Multipart annotation is optional\n  // when your parameter is of type MultipartField...\n  MultipartField document,\n  // ...but is useful if you need to massage the name.\n  @Multipart(name=\"extra\") List\u003cMultipartField\u003e supplements,\n  // If you specify type byte[] for a @Multipart field,\n  // you'll get just its binary data injected\n  @Multipart(optional=true) byte[] photo\n) {\n  // Let's demonstrate the functionality MultipartField provides.\n\n  // Form field name, always available, e.g. \"document\"\n  String name = document.getName();\n  // Browser may provide this for files, e.g. \"test.pdf\"\n  Optional\u003cString\u003e filename = document.getFilename();  \n  // Browser may provide this for files, e.g. \"application/pdf\"\n  Optional\u003cString\u003e contentType = document.getContentType();\n  // Field data as bytes, if available\n  Optional\u003cbyte[]\u003e data = document.getData();\n  // Field data as a string, if available\n  Optional\u003cString\u003e dataAsString = document.getDataAsString();\n\n  // Apply the standard redirect-after-POST pattern\n  return Response.withRedirect(\n    RedirectType.HTTP_307_TEMPORARY_REDIRECT, \"/thanks\"\n  ).build();  \n}\n```\n\n#### Dependency Injection\n\nIn practice, you will likely want to tie in to whatever Dependency Injection library your application uses and have the DI infrastructure vend your instances.\n\nSoklet integrates via an [`InstanceProvider`](https://javadoc.soklet.com/com/soklet/InstanceProvider.html).\n\nHere's how it might look if you use [Google Guice](https://github.com/google/guice):\n\n```java\n// Standard Guice setup\nInjector injector = Guice.createInjector(new MyExampleAppModule());\n\nSokletConfig config = SokletConfig.withServer(\n  Server.fromPort(8080)\n).instanceProvider(new InstanceProvider() {\n  @NonNull\n  @Override  \n  public \u003cT\u003e T provide(@NonNull Class\u003cT\u003e instanceClass) {\n    // Have Soklet ask the Guice Injector for the instance\n    return injector.getInstance(instanceClass);     \n  }\n}).build();\n```\n\nNow, your Resources are dependency-injected just like the rest of your application is:\n\n```java\npublic class WidgetResource {\n  private WidgetService widgetService;\n\n  @Inject\n  public WidgetResource(WidgetService widgetService) {\n    this.widgetService = widgetService;\n  }\n\n  @GET(\"/widgets\")\n  public List\u003cWidget\u003e widgets() {\n    return widgetService.findWidgets();\n  }\n}\n```\n\n#### Lifecycle Handling and Interception\n\nImplement [`LifecycleObserver`](https://javadoc.soklet.com/com/soklet/LifecycleObserver.html) and\n[`RequestInterceptor`](https://javadoc.soklet.com/com/soklet/RequestInterceptor.html) to hook into server and request lifecycles.\n\nServer Start/Stop: execute code immediately before and after [`Server`](https://javadoc.soklet.com/com/soklet/Server.html) startup and shutdown.\n\n```java\nSokletConfig config = SokletConfig.withServer(\n  Server.fromPort(8080)\n).lifecycleObserver(new LifecycleObserver() {\n  @Override\n  public void willStartServer(@NonNull Server server) {\n    // Perform startup tasks required prior to server launch\n    MyPayrollSystem.INSTANCE.startLengthyWarmupProcess();\n  }\n\n  @Override\n  public void didStartServer(@NonNull Server server) {\n    // Server has fully started up and is listening\n    System.out.println(\"Server started.\");\n  }\n\n  @Override\n  public void willStopServer(@NonNull Server server) {\n    // Perform shutdown tasks required prior to server teardown\n    MyPayrollSystem.INSTANCE.destroy();    \n  }\n\n  @Override\n  public void didStopServer(@NonNull Server server) {\n    // Server has fully shut down\n    System.out.println(\"Server stopped.\");\n  }\n}).build();\n```\n\nRequest Handling: these methods are fired at the very start of [`Request`](https://javadoc.soklet.com/com/soklet/Request.html) processing and the very end, respectively.\n\n```java\nSokletConfig config = SokletConfig.withServer(\n  Server.fromPort(8080)\n).lifecycleObserver(new LifecycleObserver() {\n  @Override\n  public void didStartRequestHandling(\n    @NonNull ServerType serverType,\n    @NonNull Request request,\n    @Nullable ResourceMethod resourceMethod\n  ) {\n    System.out.printf(\"Received request: %s\\n\", request);\n\n    // If there was no resourceMethod matching the request, expect a 404\n    if(resourceMethod != null)\n      System.out.printf(\"Request to be handled by: %s\\n\", resourceMethod);\n    else\n      System.out.println(\"This will be a 404.\");\n  }\n\n  @Override\n  public void didFinishRequestHandling(\n    @NonNull ServerType serverType,\n    @NonNull Request request,\n    @Nullable ResourceMethod resourceMethod,\n    @NonNull MarshaledResponse marshaledResponse,\n    @NonNull Duration processingDuration,\n    @NonNull List\u003cThrowable\u003e throwables\n  ) {\n    // We have access to a few things here...\n    // * marshaledResponse is what was ultimately sent\n    //    over the wire\n    // * processingDuration is how long everything took, \n    //    including sending the response to the client\n    // * throwables is the ordered list of exceptions\n    //    thrown during execution (if any)\n    long millis = processingDuration.toMillis();\n    System.out.printf(\"Entire request took %dms\\n\", millis);\n  }\n}).build();                  \n```\n\nRequest Wrapping: wraps around the whole \"outside\" of an entire [`Request`](https://javadoc.soklet.com/com/soklet/Request.html) handling flow.\n\nRequest wrapping runs before Soklet resolves which [`ResourceMethod`](https://javadoc.soklet.com/com/soklet/ResourceMethod.html) should handle the request. If you want to rewrite the HTTP method or path, return a modified [`Request`](https://javadoc.soklet.com/com/soklet/Request.html) via the consumer and Soklet will route using the wrapped request. You must call `requestProcessor.accept(...)` exactly once before returning; otherwise Soklet logs an error and returns a 500 response.\n\n```java\n// Special scoped value so anyone can access the current Locale.\n// For Java \u003c 21, use ThreadLocal instead\npublic static final ScopedValue\u003cLocale\u003e CURRENT_LOCALE;\n\n// Spin up the ScopedValue (or ThreadLocal)\nstatic {\n  CURRENT_LOCALE = ScopedValue.newInstance();\n}\n\nSokletConfig config = SokletConfig.withServer(\n  Server.fromPort(8080)\n).requestInterceptor(new RequestInterceptor() {\n  @Override\n  public void wrapRequest(\n    @NonNull ServerType serverType,\n    @NonNull Request request,\n    @NonNull Consumer\u003cRequest\u003e requestProcessor\n  ) {\n    // Make the locale accessible by other code during this request...\n    Locale locale = request.getLocales().get(0);\n    \n    // ...by binding it to a ScopedValue (or ThreadLocal).\n    ScopedValue.where(CURRENT_LOCALE, locale).run(() -\u003e {\n      // You must call this so downstream processing can proceed\n      requestProcessor.accept(request);\n    });\n  }\n}).build();\n\n// Then, elsewhere in your code while a request is being processed:\n\nclass ExampleService {\n  void accessCurrentLocale() {\n    // You now have access to the Locale bound to the logical scope\n    // (or Thread) without having to pass it down the call stack\n    Locale locale = CURRENT_LOCALE.orElse(Locale.getDefault());\n  }\n}\n```\n\nRequest Intercepting (via [`RequestInterceptor`](https://javadoc.soklet.com/com/soklet/RequestInterceptor.html)): provides programmatic control over two processing steps.\n\n1. Invoking the appropriate [`ResourceMethod`](https://javadoc.soklet.com/com/soklet/ResourceMethod.html) to acquire a [`MarshaledResponse`](https://javadoc.soklet.com/com/soklet/MarshaledResponse.html)\n2. Sending the [`MarshaledResponse`](https://javadoc.soklet.com/com/soklet/MarshaledResponse.html) over the wire to the client\n\nYou must call `responseWriter.accept(...)` exactly once before returning; otherwise Soklet logs an error and returns a 500 response.\n\n```java\nSokletConfig config = SokletConfig.withServer(\n  Server.fromPort(8080)\n).requestInterceptor(new RequestInterceptor() {\n  @Override\n  public void interceptRequest(\n    @NonNull ServerType serverType,\n    @NonNull Request request,\n    @Nullable ResourceMethod resourceMethod,\n    @NonNull Function\u003cRequest, MarshaledResponse\u003e responseGenerator,\n    @NonNull Consumer\u003cMarshaledResponse\u003e responseWriter\n  ) {\n    // Here's where you might start a DB transaction.\n    // (MyDatabase is a hypothetical construct)\n    MyDatabase.INSTANCE.beginTransaction();\n\n    // Step 1: Invoke the Resource Method and acquire its response\n    MarshaledResponse response = responseGenerator.apply(request);\n\n    // Commit the DB transaction before sending the response\n    // to reduce contention by keeping \"open\" time short\n    MyDatabase.INSTANCE.commitTransaction();\n\n    // Set a special header on the response via mutable copy\n    response = response.copy().headers((mutableHeaders) -\u003e {\n      mutableHeaders.put(\"X-Powered-By\", Set.of(\"Soklet\"));\n    }).finish();\n\n    // Step 2: Send the finalized response over the wire\n    responseWriter.accept(response);\n  }\n}).build();\n```\n\nResponse Writing: monitor the response writing process for each [`MarshaledResponse`](https://javadoc.soklet.com/com/soklet/MarshaledResponse.html) - sending bytes over the wire - which may terminate exceptionally (e.g. unexpected client disconnect).\n\n```java\nSokletConfig config = SokletConfig.withServer(\n  Server.fromPort(8080)\n).lifecycleObserver(new LifecycleObserver() {\n  @Override\n  public void willStartResponseWriting(\n    @NonNull Request request,\n    @Nullable ResourceMethod resourceMethod,\n    @NonNull MarshaledResponse marshaledResponse\n  ) {\n    // Access to marshaledResponse here lets us see exactly\n    // what will be going over the wire\n    byte[] body = marshaledResponse.getBody().orElse(new byte[] {});\n    System.out.printf(\"About to start writing response with \" + \n      \"a %d-byte body...\\n\", body.length);\n  }\n\n  @Override\n  public void didFinishResponseWriting(\n    @NonNull Request request,\n    @Nullable ResourceMethod resourceMethod,\n    @NonNull MarshaledResponse marshaledResponse,\n    @NonNull Duration responseWriteDuration,\n    @Nullable Throwable throwable\n  ) {\n    long millis = responseWriteDuration.toMillis();\n    System.out.printf(\"Took %dms to write response\\n\", millis);\n\n    // You have access to the throwable that might have occurred\n    // while writing the response.  This is useful to, for example,\n    // determine trends in unexpected client disconnect rates\n    if(throwable != null) {\n      System.err.println(\"Exception occurred while writing response\");\n      throwable.printStackTrace();\n    }\n  }\n}).build();\n```\n\n#### CORS Support\n\nCORS is handled by [`CorsAuthorizer`](https://javadoc.soklet.com/com/soklet/CorsAuthorizer.html) using\n[`Cors`](https://javadoc.soklet.com/com/soklet/Cors.html) metadata and returns\n[`CorsPreflightResponse`](https://javadoc.soklet.com/com/soklet/CorsPreflightResponse.html) /\n[`CorsResponse`](https://javadoc.soklet.com/com/soklet/CorsResponse.html) as needed.\n\nAuthorize All Origins:\n\n```java\nSokletConfig config = SokletConfig.withServer(server)\n  // \"Wildcard\" (*) CORS authorization. Don't use this in production!\n  .corsAuthorizer(CorsAuthorizer.acceptAllInstance())\n  .build();\n```\n\nAuthorize Whitelisted Origins:\n\n```java\nSet\u003cString\u003e allowedOrigins = Set.of(\"https://www.revetware.com\");\n\nSokletConfig config = SokletConfig.withServer(server)\n  .corsAuthorizer(WhitelistedOriginsCorsAuthorizer.fromOrigins(allowedOrigins))\n  .build();\n```\n\n...or be dynamic:\n\n```java\nSokletConfig config = SokletConfig.withServer(server)\n  .corsAuthorizer(WhitelistedOriginsCorsAuthorizer.fromAuthorizer(\n    (origin) -\u003e origin.equals(\"https://www.revetware.com\")\n  ))\n  .build();\n```\n\nCustom CORS logic:\n\n```java\nSokletConfig config = SokletConfig.withServer(server)\n  .corsAuthorizer(new CorsAuthorizer() {\n    // Any subdomain under soklet.com is permitted\n    boolean originMatchesValidSubdomain(@NonNull Cors cors) {\n      return cors.getOrigin().matches(\"https://(.+)\\\\.soklet\\\\.com\");\n    }\n\n    @NonNull\n    @Override\n    public Optional\u003cCorsPreflightResponse\u003e authorizePreflight(\n      @NonNull Request request,\n      @NonNull Map\u003cHttpMethod, ResourceMethod\u003e availableResourceMethodsByHttpMethod\n    ) {\n      // Requests here are guaranteed to have the Cors value set\n      Cors cors = request.getCors().orElseThrow();\n\n      // Only greenlight our soklet.com subdomains\n      if (originMatchesValidSubdomain(cors))\n        return Optional.of(\n          CorsPreflightResponse.withAccessControlAllowOrigin(cors.getOrigin())\n            .accessControlAllowMethods(availableResourceMethodsByHttpMethod.keySet())\n            .accessControlAllowHeaders(Set.of(\"*\"))\n            .accessControlAllowCredentials(true)\n            .accessControlMaxAge(Duration.ofMinutes(10))        \n            .build()\n        );\n\n      return Optional.empty();\n    }    \n\n    @NonNull\n    @Override\n    public Optional\u003cCorsResponse\u003e authorize(@NonNull Request request) {\n      // Requests here are guaranteed to have the Cors value set\n      Cors cors = request.getCors().orElseThrow();\n\n      // Only greenlight our soklet.com subdomains\n      if (originMatchesValidSubdomain(cors))\n        return Optional.of(\n          CorsResponse.withAccessControlAllowOrigin(cors.getOrigin())\n            .accessControlExposeHeaders(Set.of(\"*\"))\n            .build()\n        );\n\n      return Optional.empty();\n    }\n  })\n  .build();\n```\n\n#### Unit Testing\n\nFirst, define something to test:\n\n```java\npublic class ReverseResource {\n  // Reverse the input\n  @POST(\"/reverse\")\n  public List\u003cInteger\u003e reverse(@RequestBody List\u003cInteger\u003e numbers) {\n    return numbers.reversed();\n  }\n\n  // Reverse the input and set custom headers/cookies\n  @POST(\"/reverse-again\")\n  public Response reverseAgain(@RequestBody List\u003cInteger\u003e numbers) {\n    Integer largest = Collections.max(numbers);\n    Instant lastRequest = Instant.now();\n\n    return Response.withStatusCode(200)\n      .headers(Map.of(\"X-Largest\", Set.of(String.valueOf(largest))))\n      .cookies(Set.of(\n        ResponseCookie.with(\"lastRequest\", lastRequest.toString()).build()\n      ))\n      .body(numbers.reversed())\n      .build();\n  }\n}\n```\n\nPerform tests:\n\n```java\nimport org.junit.Assert;\nimport org.junit.Test;\n\n@Test\npublic void reverseUnitTest() {\n  // Your Resource is a Plain Old Java Object, no Soklet dependency\n  ReverseResource resource = new ReverseResource();\n\n  List\u003cInteger\u003e input = List.of(1, 2, 3);\n  List\u003cInteger\u003e expected = List.of(3, 2, 1);\n  List\u003cInteger\u003e actual = resource.reverse(input);\n\n  Assert.assertEquals(\"Reverse failed\", expected, actual);\n}\n\n@Test\npublic void reverseAgainUnitTest() {\n  ReverseResource resource = new ReverseResource();\n  List\u003cInteger\u003e input = List.of(1, 2, 3);\n\n  // Set expectations\n  List\u003cInteger\u003e expectedBody = List.of(3, 2, 1);\n  Integer expectedCode = 200;\n  Integer expectedLargest = Collections.max(input);\n  Instant lastRequestAfter = Instant.now();\n\n  Response response = resource.reverseAgain(input);\n\n  // Extract actuals\n  Integer actualCode = response.getStatusCode();\n  List\u003cInteger\u003e actualBody = (List\u003cInteger\u003e) response.getBody().orElseThrow();\n\n  Integer actualLargest = response.getHeaders().get(\"X-Largest\").stream()\n    .findAny()\n    .map(value -\u003e Integer.valueOf(value))\n    .orElseThrow();\n\n  Instant actualLastRequest = response.getCookies().stream()\n    .filter(responseCookie -\u003e responseCookie.getName().equals(\"lastRequest\"))\n    .findAny()\n    .map(responseCookie -\u003e Instant.parse(responseCookie.getValue().orElseThrow()))\n    .orElseThrow();\n\n  // Verify expectations vs. actuals\n  Assert.assertEquals(\"Bad status code\", expectedCode, actualCode);\n  Assert.assertEquals(\"Reverse failed\", expectedBody, actualBody);\n  Assert.assertEquals(\"Largest header failed\", expectedLargest, actualLargest);\n  Assert.assertTrue(\"Last request too early\", actualLastRequest.isAfter(lastRequestAfter));  \n}\n```\n\n#### Integration Testing\n\nFirst, define something to test:\n\n```java\npublic class HelloResource {\n  // Hypothetical service that performs business logic\n  private HelloService helloService;\n\n  public HelloResource(HelloService helloService) {\n    this.helloService = helloService;\n  }\n\n  // Respond with a 'hello' message, e.g. Hello, Mark\n  @GET(\"/hello\")\n  public String hello(@QueryParameter String name) {\n    return this.helloService.sayHelloTo(name);\n  }\n}\n```\n\nPerform tests:\n\nSoklet's [`Simulator`](https://javadoc.soklet.com/com/soklet/Simulator.html) is available via [`Soklet`](https://javadoc.soklet.com/com/soklet/Soklet.html) to exercise full request/response flows without binding a port.\n\n```java\n@Test\npublic void basicIntegrationTest() {\n  // Just use your app's existing configuration\n  SokletConfig config = obtainMySokletConfig();\n\n  // Instead of running in a real HTTP server that listens on a port,\n  // a simulator is provided against which you can issue requests\n  // and receive responses.\n  Soklet.runSimulator(config, (simulator -\u003e {\n    // Construct a request\n    Request request = Request.withPath(HttpMethod.GET, \"/hello\")\n      .queryParameters(Map.of(\"name\", Set.of(\"Mark\")))\n      .build();\n\n    // Perform the request and get a handle to the response\n    MarshaledResponse marshaledResponse = simulator.performRequest(request);\n    \n    // Verify status code\n    Integer expectedCode = 200;\n    Integer actualCode = marshaledResponse.getStatusCode();\n    Assert.assertEquals(\"Bad status code\", expectedCode, actualCode);\n\n    // Verify response body\n    marshaledResponse.getBody().ifPresentOrElse(body -\u003e {\n      String expectedBody = \"Hello, Mark\";\n      String actualBody = new String(body, StandardCharsets.UTF_8);\n      Assert.assertEquals(\"Bad response body\", expectedBody, actualBody);\n    }, () -\u003e {\n      Assert.fail(\"No response body\");\n    });\n  }));\n}\n```\n\n#### Metrics Collection\n\nSoklet includes a [`MetricsCollector`](https://javadoc.soklet.com/com/soklet/MetricsCollector.html) hook for collecting HTTP and SSE telemetry. The default in-memory\ncollector is enabled automatically, but you can replace or disable it:\n\n```java\nSokletConfig config = SokletConfig.withServer(\n  Server.fromPort(8080)\n).metricsCollector(\n  MetricsCollector.defaultInstance()\n  // or MetricsCollector.disabledInstance()\n).build();\n```\n\nUse [`MetricsCollector.SnapshotTextOptions`](https://javadoc.soklet.com/com/soklet/MetricsCollector.SnapshotTextOptions.html) and\n[`MetricsCollector.MetricsFormat`](https://javadoc.soklet.com/com/soklet/MetricsCollector.MetricsFormat.html) to control text output.\n\nYou can expose a `/metrics` endpoint by injecting [`MetricsCollector`](https://javadoc.soklet.com/com/soklet/MetricsCollector.html)\ninto a [`ResourceMethod`](https://javadoc.soklet.com/com/soklet/ResourceMethod.html):\n\n```java\n@GET(\"/metrics\")\npublic MarshaledResponse getMetrics(@NonNull MetricsCollector metricsCollector) {\n  SnapshotTextOptions options = SnapshotTextOptions\n    .withMetricsFormat(MetricsFormat.PROMETHEUS)\n    .histogramFormat(HistogramFormat.FULL_BUCKETS)\n    .includeZeroBuckets(false)\n    .build();\n\n  String body = metricsCollector.snapshotText(options).orElse(null);\n\n  if (body == null)\n    return MarshaledResponse.fromStatusCode(204);\n\n  return MarshaledResponse.withStatusCode(200)\n    .headers(Map.of(\"Content-Type\", Set.of(\"text/plain; charset=UTF-8\")))\n    .body(body.getBytes(StandardCharsets.UTF_8))\n    .build();\n}\n```\n\n#### Servlet Integration\n\nOptional support is available for both legacy [`javax.servlet`](https://github.com/soklet/soklet-servlet-javax) and current [`jakarta.servlet`](https://github.com/soklet/soklet-servlet-jakarta) specifications.  Just add the appropriate JAR to your project and you're good to go.\n\nThe Soklet website has in-depth [Servlet integration documentation](https://www.soklet.com/docs/servlet-integration).\n\n### Learning More\n\nPlease refer to the official Soklet website [https://www.soklet.com](https://www.soklet.com) for detailed documentation.\n\n### Credits\n\nSoklet stands on the shoulders of giants.  Internally, it embeds code from the following OSS projects:\n\n* [Microhttp](https://github.com/ebarlas/microhttp) by [Elliot Barlas](https://github.com/ebarlas) - MIT License\n* [Selenium](https://github.com/SeleniumHQ/selenium) - Apache 2.0 License\n* [Apache Commons FileUpload](https://commons.apache.org/proper/commons-fileupload/) - Apache 2.0 License\n* [The Spring Framework](https://spring.io/) - Apache 2.0 License\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoklet%2Fsoklet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoklet%2Fsoklet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoklet%2Fsoklet/lists"}