What Is It?
A small HTTP/1.1 server and route handler for Java, well-suited for building RESTful APIs, broadcasting Server-Sent Events, and providing Model Context Protocol (MCP) functionality. Zero dependencies. Dependency Injection friendly. Optionally powered by JEP 444: Virtual Threads.
Soklet codes like a library, not a framework.
Note: this README provides a high-level overview of Soklet. For details, please refer to the official documentation at https://www.soklet.com.
Why?
The Java web ecosystem is missing an HTTP server solution that is dependency-free but offers support for Server-Sent Events (SSE) and Model Context Protocol (MCP) along with hooks for dependency injection and annotation-based request handling. Soklet aims to fill this void.
Soklet provides the plumbing to build "transactional" REST APIs as well as agentic systems that vend results via HTTP response streaming or SSE, and expose tools, prompts, and resources via MCP. It does not make technology choices on your behalf (but an example of how to build a full-featured API is available). It does not natively support Reactive Programming or similar methodologies. It does give you the foundation to build your system, your way.
Soklet is commercially-friendly Open Source Software, proudly powering production systems since 2015.
Design Goals
- Main focus: routing HTTP/1.1 requests to Java methods
- Near-instant startup
- Zero dependencies
- Immutability/thread-safety
- Small, comprehensible codebase - auditable end-to-end by a human or AI agent
- No runtime classpath scanning or autoconfiguration (explicit behavior, statically analyzable)
- Contract/interface-driven: bring your own implementations for almost anything
- Thorough, high-quality documentation
- Extensive support for automated unit and integration testing
- Fine-grained telemetry and metrics collection
- Best-in-class support for Server-Sent Events
- First-class support for Model Context Protocol (MCP)
- Servlet Integration for legacy code
Design Non-Goals
- SSL/TLS (your load balancer should provide TLS termination)
- HTTP/2, HTTP/3 (also handled by your load balancer)
- WebSockets
- Dictate which technologies to use (Guice vs. Dagger, Gson vs. Jackson, etc.)
- "Batteries included" authentication and authorization
Do Zero-Dependency Libraries Interest You?
Similarly-flavored commercially-friendly OSS libraries are available.
- Pyranid - makes working with JDBC pleasant
- Lokalized - natural-sounding translations (i18n) via expression language
License
Installation
Soklet is a single JAR, available on Maven Central.
JDK 17+ is required (or JDK 21+ for Server-Sent Events and MCP).
Maven
<dependency>
<groupId>com.soklet</groupId>
<artifactId>soklet</artifactId>
<version>3.5.1</version>
</dependency>
Gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'com.soklet:soklet:3.5.1'
}
Direct Download
If you don't use Maven or Gradle, you can drop soklet-3.5.1.jar directly into your project. No other dependencies are required.
Code Sample
Here we demonstrate building and running a single-file Soklet application with nothing but the soklet-3.5.1.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.
Soklet systems can be structurally as simple as a "hello world" app.
While a real production system will have more moving parts, this demonstrates that you can build server software without ceremony or dependencies.
package com.soklet.example;
public class App {
// Canonical example
@GET("/")
public String index() {
return "Hello, world!";
}
// Echoes back the path parameter, which must be a LocalDate
@GET("/echo/{date}")
public LocalDate echo(@PathParameter LocalDate date) {
return date;
}
// Formats request body locale for display and customizes the response.
// Example: fr-CA ⇒ francês (Canadá)
@POST("/language")
public Response languageFor(@RequestBody Locale locale) {
Locale systemLocale = Locale.forLanguageTag("pt-BR");
String contentLanguage = systemLocale.toLanguageTag();
return Response.withStatusCode(200)
.body(locale.getDisplayName(systemLocale))
.headers(Map.of("Content-Language", Set.of(contentLanguage)))
.cookies(Set.of(
ResponseCookie.withName("lastRequest")
.value(Instant.now().toString())
.httpOnly(true)
.secure(true)
.maxAge(Duration.ofMinutes(5))
.sameSite(SameSite.LAX)
.build()
))
.build();
}
// Start the server and listen on :8080
public static void main(String[] args) throws Exception {
// Use out-of-the-box defaults
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).build();
try (Soklet soklet = Soklet.fromConfig(config)) {
soklet.start();
System.out.println("Soklet started, press [enter] to exit");
soklet.awaitShutdown(ShutdownTrigger.ENTER_KEY);
}
}
}
Here we use raw javac to build and java to run.
This example requires JDK 17+ to be installed on your machine (or see this example of using Docker for Soklet apps). If you need a JDK, Amazon provides Corretto - a free-to-use-commercially, production-ready distribution of OpenJDK that includes long-term support.
Build
javac -parameters -cp soklet-3.5.1.jar -processor com.soklet.SokletProcessor -d build src/com/soklet/example/App.java
Run
java -cp soklet-3.5.1.jar:build com/soklet/example/App
Test
# Hello, world
% curl -i 'http://localhost:8080/'
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Hello, world!
# Acceptable path parameter
% curl -i 'http://localhost:8080/echo/2024-12-31'
HTTP/1.1 200 OK
Content-Length: 10
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
2024-12-31
# Illegal path parameter
% curl -i 'http://localhost:8080/echo/abc'
HTTP/1.1 400 Bad Request
Content-Length: 21
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
HTTP 400: Bad Request
# Language request body
% curl -i -X POST 'http://localhost:8080/language' -d 'fr-CA'
HTTP/1.1 200 OK
Content-Language: pt-BR
Content-Length: 18
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
Set-Cookie: lastRequest=2024-04-21T16:19:01.115336Z; Max-Age=300; Secure; HttpOnly; SameSite=Lax
francês (Canadá)
Building Real-World Apps
Of course, real-world apps have more moving parts than a "hello world" example.
The Toy Store App showcases how you might build a robust production system with Soklet.
Feature highlights include:
- Authentication and role-based authorization
- Basic CRUD operations
- Dependency injection via Google Guice
- Relational database integration via Pyranid
- Context-awareness via ScopedValue (JEP 481)
- Internationalization via the JDK and Lokalized
- JSON requests/responses via Gson
- Logging via SLF4J / Logback
- Metrics collection via
MetricsCollector - Automated unit and integration tests via JUnit
- Ability to run in Docker
What Else Does It Do?
Request Handling
Soklet maps HTTP requests to plain Java methods known as Resource Methods
(ResourceMethod).
Annotate them with @GET,
@POST,
@PUT,
@PATCH,
@DELETE,
@HEAD,
@OPTIONS, or
@SseEventSource for SSE.
MCP endpoints are declared separately with @McpServerEndpoint
and handler annotations like @McpTool,
@McpPrompt, and
@McpResource.
Soklet discovers them at compile time via the
SokletProcessor annotation processor, avoiding
classpath scans at startup. See the Request Handling docs for details.
Access To Request Data
Resource Methods (ResourceMethod) can accept a
Request parameter and inspect
HttpMethod values.
@GET("/example")
public void example(Request request /* param name is arbitrary */) {
// Here, it would be HttpMethod.GET
HttpMethod httpMethod = request.getHttpMethod();
// Just the path, e.g. "/example"
String path = request.getPath();
// The raw path and query, e.g. "/example?test=123"
String rawPathAndQuery = request.getRawPathAndQuery();
// Request body as bytes, if available
Optional<byte[]> body = request.getBody();
// Request body marshaled to a string, if available.
// Charset defined in "Content-Type" header is used to marshal.
// If not specified, UTF-8 is assumed
Optional<String> bodyAsString = request.getBodyAsString();
// Query parameter values by name
Map<String, Set<String>> queryParameters = request.getQueryParameters();
// Shorthand for plucking the first query param value by name
Optional<String> queryParameter = request.getQueryParameter("test");
// Header values by name (names are case-insensitive)
Map<String, Set<String>> headers = request.getHeaders();
// Shorthand for plucking the first header value by name (case-insensitive)
Optional<String> header = request.getHeader("Accept-Language");
// Parsed W3C trace context from traceparent/tracestate, if present
Optional<TraceContext> traceContext = request.getTraceContext();
// Request cookies by name (names are case-insensitive)
Map<String, Set<String>> cookies = request.getCookies();
// Shorthand for plucking the first cookie value by name (case-insensitive)
Optional<String> cookie = request.getCookie("cookie-name");
// Form parameters by name (application/x-www-form-urlencoded)
Map<String, Set<String>> fps = request.getFormParameters();
// Shorthand for plucking the first form parameter value by name
Optional<String> fp = request.getFormParameter("fp-name");
// Is this a multipart request?
boolean multipart = request.isMultipart();
// Multipart fields by name
Map<String, Set<MultipartField>> mpfs = request.getMultipartFields();
// Shorthand for plucking the first multipart field by name
Optional<MultipartField> mpf = request.getMultipartField("file-input");
// CORS information, if available
Optional<Cors> cors = request.getCors();
// Ordered locales via Accept-Language parsing
List<Locale> locales = request.getLocales();
// Ordered media ranges via Accept parsing; empty means no Accept preference
List<MediaRange> mediaRanges = request.getMediaRanges();
// Charset as specified by "Content-Type" header, if available
Optional<Charset> charset = request.getCharset();
// Content type component of "Content-Type" header, if available
Optional<String> contentType = request.getContentType();
}
Value Conversions
Soklet converts textual request inputs to Java types using a
ValueConverterRegistry populated with
ValueConverter<F,T>.
Conversions are applied to parameters annotated with
@QueryParameter,
@PathParameter,
@RequestHeader,
@RequestCookie,
@FormParameter, and
@Multipart.
Supply your own registry (or additional converters) via
SokletConfig to support custom types.
Request Body Parsing
Configure a RequestBodyMarshaler however you like - here we accept JSON:
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).requestBodyMarshaler(new RequestBodyMarshaler() {
// This example uses Google's GSON
static final Gson GSON = new Gson();
@NonNull
@Override
public Optional<Object> marshalRequestBody(
@NonNull Request request,
@NonNull ResourceMethod resourceMethod,
@NonNull Parameter parameter,
@NonNull Type requestBodyType
) {
// Let GSON turn the request body into an instance
// of the specified type.
//
// Note that this method has access to all runtime information
// about the request, which provides the opportunity to, for example,
// examine annotations on the method/parameter which might
// inform custom marshaling strategies.
return Optional.of(GSON.fromJson(
request.getBodyAsString().orElseThrow(),
requestBodyType
));
}
}).build();
Then, apply:
public record Employee (
UUID id,
String name
) {}
// Accepts a JSON-formatted Record type as input
@POST("/employees")
public void createEmployee(@RequestBody Employee employee) {
System.out.printf("TODO: create %s\n", employee.name());
}
Response Writing
To control how response data is surfaced to clients (e.g. JSON), provide handler functions
(ResourceMethodHandler and
ThrowableHandler) to Soklet as shown below.
Alternatively, you can provide your own implementation of ResponseMarshaler for full control.
// 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();
};
// 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, 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 handlers to the standard response marshaler
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).responseMarshaler(ResponseMarshaler.builder()
.resourceMethod(resourceMethodHandler)
.throwable(throwableHandler)
.build()
).build();
Zero-Copy Responses
Already know exactly what you want to send over the wire? Use MarshaledResponse to skip additional processing.
@GET("/example-image.png")
public MarshaledResponse exampleImage() {
Path imageFile = Path.of("/home/user/test.png");
// Serve a known-length file response over the wire.
// Soklet sets Content-Length from the file size; Content-Type remains explicit.
return MarshaledResponse.withStatusCode(200)
.body(imageFile)
.headers(Map.of(
"Content-Type", Set.of("image/png")
))
.build();
}
MarshaledResponse supports known-length byte-array, file, file-channel, and ByteBuffer bodies. The standard HTTP server can write file-backed responses without first loading the whole file into heap memory. If you already selected a trusted file and want file-response semantics like validators and byte ranges, use MarshaledResponse::withFile; its builder can set Content-Type, Content-Encoding, cache headers, validators, and range behavior. For safe static roots, use StaticFiles instead of hand-rolled path joins; it handles root containment, validators, optional content-hash ETags, access policy, single byte ranges, MIME defaults, and GET/HEAD behavior. Standard HTTP can also opt into gzip compression for finalized in-memory byte-array and ByteBuffer responses with HttpServer.Builder::responseGzipPolicy, including ResponseGzipPolicy::fromDefaultsWithMinimumBodySizeInBytes for common text-like response media types.
Streaming Responses
MarshaledResponse also supports streaming response bodies when the final byte length is not known up front. Streaming is intentionally a marshaled-response feature, like file-backed output: the resource method is taking direct control of what Soklet writes to the HTTP response.
@GET("/tokens")
public MarshaledResponse tokens(TokenService tokenService) {
return MarshaledResponse.withStatusCode(200)
.headers(Map.of(
"Content-Type", Set.of("text/plain; charset=UTF-8"),
"Cache-Control", Set.of("no-transform")
))
.stream(StreamingResponseBody.fromWriter((output, context) -> {
try (AutoCloseable ignored = context.onCancel(tokenService::stop)) {
tokenService.generate(token -> {
context.throwIfCanceled();
output.write(token.getBytes(StandardCharsets.UTF_8));
output.flush();
});
}
}))
.build();
}
Streaming responses use HTTP/1.1 chunked transfer encoding. Soklet owns Transfer-Encoding, rejects caller-supplied Content-Length, and gives the producer a StreamingResponseContext so upstream work can be canceled when the client disconnects, the server shuts down, or a streaming timeout fires. That context also exposes the originating Request, so producers can use Request::getId for correlation without ambient thread-local state.
Redirects (via Response):
@GET("/example-redirect")
public Response exampleRedirect() {
// Response has a convenience builder for performing redirects.
// You could alternatively do this "by hand" by setting HTTP status
// and headers appropriately.
return Response.withRedirect(
RedirectType.HTTP_307_TEMPORARY_REDIRECT, "/other-url"
).build();
}
HTTP Server Configuration
Soklet ships with an embedded HTTP/1.1 HttpServer, a dedicated
SseServer, and a dedicated
McpServer.
These builders let you configure host, read/write/handler timeouts, handler concurrency/queueing, request size limits, request decompression, and connection caps; you
can also plug in custom request/session IdGenerator,
McpSessionStore, and
MultipartParser instances.
Standard HTTP request-body decompression is disabled by default; enable HttpServer.Builder::requestDecompressionPolicy with RequestDecompressionPolicy::fromDefaults or a custom policy to accept single-coding Content-Encoding: gzip/x-gzip request bodies with decompression-bomb limits. Handlers receive the decompressed bytes through Request::getBody, while Request::getEncodedBodySizeInBytes retains the pre-decompression payload size for telemetry.
Configure MCP session ID generation and session caps on the default McpSessionStore builder, or let a custom store own those policies.
Provide the configured servers via SokletConfig and see the
Server Configuration docs for the full option matrix.
Server-Sent Events (SSE)
SSE endpoints are declared with @SseEventSource and return a
SseHandshakeResult, served from a dedicated
SseServer port (separate from your standard HTTP server port).
public record ChatMessage(String message) {}
public class ChatResource {
@SseEventSource("/chat")
public SseHandshakeResult chat() {
return SseHandshakeResult.Accepted.builder()
.clientInitializer(unicaster -> {
unicaster.unicastEvent(SseEvent.withEvent("hello")
.data("welcome")
.build());
})
.build();
}
@POST("/chat")
public void postMessage(@RequestBody ChatMessage message,
@NonNull SseServer sseServer) {
@NonNull SseBroadcaster broadcaster = sseServer
.acquireBroadcaster(ResourcePath.fromPath("/chat"))
.orElseThrow();
broadcaster.broadcastEvent(SseEvent.withEvent("message")
.data(message.message())
.build());
}
}
Because this example exposes both an SSE event source and a regular POST /chat
resource method, it needs both servers:
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).sseServer(
SseServer.fromPort(8081)
).resourceMethodResolver(
ResourceMethodResolver.fromClasses(Set.of(ChatResource.class))
).build();
If your application only exposes SSE event source methods, you can omit the regular
HTTP server and start with SokletConfig::withSseServer instead.
SSE test via the Simulator
(see SseRequestResult):
import org.junit.Assert;
import org.junit.Test;
@Test
public void sseTest() {
SokletConfig config = SokletConfig.withHttpServer(HttpServer.fromPort(0).build())
.sseServer(SseServer.fromPort(0))
.resourceMethodResolver(ResourceMethodResolver.fromClasses(Set.of(ChatResource.class)))
.build();
List<SseEvent> events = new ArrayList<>();
Soklet.runSimulator(config, simulator -> {
Request request = Request.fromPath(HttpMethod.GET, "/chat");
@NonNull SseRequestResult result = simulator.performSseRequest(request);
if (result instanceof SseRequestResult.HandshakeAccepted accepted) {
accepted.registerEventConsumer(events::add);
@NonNull SseBroadcaster broadcaster = config.getSseServer().orElseThrow()
.acquireBroadcaster(ResourcePath.fromPath("/chat")).orElseThrow();
broadcaster.broadcastEvent(SseEvent.withEvent("message")
.data("hello")
.build());
} else {
throw new IllegalStateException("SSE handshake failed: " + result);
}
});
Assert.assertEquals("hello", events.get(0).getData().orElse(null));
}
Model Context Protocol (MCP)
MCP endpoints are declared with @McpServerEndpoint and expose handlers via
@McpTool,
@McpPrompt, and
@McpResource. They are served from a dedicated
McpServer port, while Soklet manages MCP session lifecycle,
JSON-RPC transport, SSE stream establishment, and simulator support.
@McpServerEndpoint(
path = "/catalog/mcp",
name = "catalog",
version = "1.0.0",
title = "Catalog MCP"
)
public class CatalogMcpEndpoint implements McpEndpoint {
@Override
public McpSessionContext initialize(McpInitializationContext context,
McpSessionContext session) {
return session.with("tenantId", "acme");
}
@McpTool(name = "lookup_recipe", description = "Looks up a recipe.")
public McpToolResult lookupRecipe(@McpArgument("recipeId") String recipeId,
McpSessionContext sessionContext) {
String tenantId = sessionContext.get("tenantId", String.class).orElseThrow();
return McpToolResult.builder()
.content(McpTextContent.fromText(
"Recipe %s for tenant %s".formatted(recipeId, tenantId)
))
.build();
}
}
Wire up an MCP-only app:
SokletConfig config = SokletConfig.withMcpServer(
McpServer.withPort(8082)
.handlerResolver(McpHandlerResolver.fromClasses(Set.of(CatalogMcpEndpoint.class)))
.build()
).build();
If the same application also serves ordinary HTTP resource methods, add
.httpServer(HttpServer.fromPort(8080)) to the builder.
Browser-based MCP clients can be enabled with
McpCorsAuthorizer:
SokletConfig config = SokletConfig.withMcpServer(
McpServer.withPort(8082)
.handlerResolver(McpHandlerResolver.fromClasses(Set.of(CatalogMcpEndpoint.class)))
.corsAuthorizer(McpCorsAuthorizer.fromWhitelistedOrigins(
Set.of("https://chat.openai.com"),
origin -> true
))
.build()
).build();
That enables OPTIONS preflight handling plus Access-Control-* response headers on MCP
POST / GET / DELETE responses for the configured origins. It also authorizes MCP
transport requests that carry an Origin header; if Origin is present and the configured
McpCorsAuthorizer does not authorize it, Soklet rejects the request with HTTP 403 before
running MCP endpoint code or mutating session/stream state. The default
nonBrowserClientsOnlyInstance() remains conservative: non-browser clients that omit
Origin work normally, while browser-originated MCP transport requests are rejected unless
you explicitly allow the origin.
Soklet's MCP v1 support is intentionally conservative: single-request JSON-RPC only, framework-managed
initialize, notifications/initialized, notifications/cancelled, and ping, cooperative
in-flight request cancelation through McpCancelationToken, framework-generated tools/list /
prompts/list / resources/templates/list responses without cursor pagination, and application-backed
pagination only for resources/list. JSON-RPC batch arrays and resumable SSE event IDs remain deferred.
MCP test via the Simulator
(see McpRequestResult):
import org.junit.Assert;
import org.junit.Test;
@Test
public void mcpTest() {
SokletConfig config = SokletConfig.withMcpServer(McpServer.withPort(0)
.handlerResolver(McpHandlerResolver.fromClasses(Set.of(CatalogMcpEndpoint.class)))
.build()
).build();
Soklet.runSimulator(config, simulator -> {
McpRequestResult initializeResult = simulator.performMcpRequest(
Request.withPath(HttpMethod.POST, "/catalog/mcp")
.headers(Map.of("Content-Type", Set.of("application/json")))
.body("""
{
"jsonrpc":"2.0",
"id":"req-1",
"method":"initialize",
"params":{
"protocolVersion":"2025-11-25",
"capabilities":{},
"clientInfo":{"name":"test-client","version":"1.0.0"}
}
}
""".getBytes(StandardCharsets.UTF_8))
.build()
);
if (!(initializeResult instanceof McpRequestResult.ResponseCompleted initializeResponse))
throw new IllegalStateException("Expected initialize to complete without opening a stream");
String sessionId = initializeResponse.getHttpRequestResult().getMarshaledResponse()
.getHeaders().get("MCP-Session-Id").iterator().next();
Assert.assertNotNull(sessionId);
});
}
Form Handling
Frontend:
<form
enctype="application/x-www-form-urlencoded"
action="https://example.soklet.com/form?id=123"
method="POST"
>
<!-- User can type whatever text they like -->
<input type="number" name="numericValue" />
<!-- Multiple values for the same name are supported -->
<input type="hidden" name="multi" value="1" />
<input type="hidden" name="multi" value="2" />
<!-- Names with special characters can be remapped -->
<textarea name="long-text"></textarea>
<!-- Note: browsers send "on" string to indicate "checked" -->
<input type="checkbox" name="enabled" />
<input type="submit" />
</form>
Backend:
Backend parameters can use @QueryParameter and
@FormParameter.
@POST("/form")
public String form(
@QueryParameter Long id,
@FormParameter Integer numericValue,
@FormParameter(optional=true) List<String> multi,
@FormParameter(name="long-text") String longText,
@FormParameter String enabled
) {
// Echo back the inputs
return List.of(id, numericValue, multi, longText, enabled).stream()
.map(Object::toString)
.collect(Collectors.joining("\n"));
}
Test:
% curl -i -X POST 'https://example.soklet.com/form?id=123' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'numericValue=456&multi=1&multi=2&long-text=long%20multiline%20text&enabled=on'
HTTP/1.1 200 OK
Content-Length: 37
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
123
456
[1, 2]
long multiline text
on
Multipart Handling
Frontend:
<form
enctype="multipart/form-data"
action="https://example.soklet.com/multipart?id=123"
method="POST"
>
<!-- User can type whatever text they like -->
<input type="text" name="freeform" />
<!-- Multiple values for the same name are supported -->
<input type="hidden" name="multi" value="1" />
<input type="hidden" name="multi" value="2" />
<!-- Prompt user to upload a file -->
<p>Please attach your document: <input name="doc" type="file" /></p>
<!-- Multiple file uploads are supported -->
<p>
Supplement 1: <input name="extra" type="file" /> Supplement 2:
<input name="extra" type="file" />
</p>
<!-- An optional file -->
<p>Optionally, attach a photo: <input name="photo" type="file" /></p>
<input type="submit" value="Upload" />
</form>
Backend:
Backend parameters can use @Multipart and
MultipartField.
@POST("/multipart")
public Response multipart(
@QueryParameter Long id,
// Multipart fields work like other Soklet params
// with support for Optional<T>, List<T>, custom names, ...
@Multipart(optional=true) String freeform,
@Multipart(name="multi") List<Integer> numbers,
// The MultipartField type allows access to additional data,
// like filename and content type (if available).
// The @Multipart annotation is optional
// when your parameter is of type MultipartField...
MultipartField document,
// ...but is useful if you need to massage the name.
@Multipart(name="extra") List<MultipartField> supplements,
// If you specify type byte[] for a @Multipart field,
// you'll get just its binary data injected
@Multipart(optional=true) byte[] photo
) {
// Let's demonstrate the functionality MultipartField provides.
// Form field name, always available, e.g. "document"
String name = document.getName();
// Browser may provide this for files, e.g. "test.pdf"
Optional<String> filename = document.getFilename();
// Browser may provide this for files, e.g. "application/pdf"
Optional<String> contentType = document.getContentType();
// Field data as bytes, if available
Optional<byte[]> data = document.getData();
// Field data as a string, if available
Optional<String> dataAsString = document.getDataAsString();
// Apply the standard redirect-after-POST pattern
return Response.withRedirect(
RedirectType.HTTP_307_TEMPORARY_REDIRECT, "/thanks"
).build();
}
Dependency Injection
In practice, you will likely want to tie in to whatever Dependency Injection library your application uses and have the DI infrastructure vend your instances.
Soklet integrates via an InstanceProvider.
Here's how it might look if you use Google Guice:
// Standard Guice setup
Injector injector = Guice.createInjector(new MyExampleAppModule());
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).instanceProvider(new InstanceProvider() {
@NonNull
@Override
public <T> T provide(@NonNull Class<T>
No comments yet
Be the first to share your take.