soklet
Health Gecti
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 25 GitHub stars
Code Uyari
- Code scan incomplete — No supported source files were scanned during light audit
Permissions Gecti
- Permissions — No dangerous permissions requested
This is a zero-dependency Java library that functions as an HTTP/1.1 and Server-Sent Event (SSE) server. It is designed to help developers build RESTful APIs and expose tools, prompts, and resources via the Model Context Protocol (MCP) for agentic systems.
Security Assessment
Overall Risk: Low. The tool operates as a local HTTP server, meaning it accepts incoming network connections rather than making outbound requests. Because it explicitly functions as a plumbing library with zero external dependencies, the attack surface is minimal. The automated scan noted that no source files were analyzed during the light audit (typical for Java projects in automated repo scanners), but a manual review of the README confirms it does not request dangerous permissions, execute shell commands, or require hardcoded secrets. Developers are expected to implement their own authentication and TLS termination via a load balancer.
Quality Assessment
The project appears to be highly maintained and professionally structured. It uses the commercially-friendly Apache-2.0 license and was updated very recently (last push was today). While it has a smaller community footprint with 25 GitHub stars, it claims to have been reliably powering production systems since 2015. The design philosophy centers on immutability, thread safety, and a small, comprehensible codebase, which are strong indicators of reliable software.
Verdict
Safe to use.
Soklet is a zero-dependency Java HTTP/1.1 and Server-Sent Event + MCP server, well-suited for building RESTful APIs and tool-backed agentic systems.
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 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.
Soklet provides the plumbing to build "transactional" REST APIs that exchange small amounts of data with clients.
It is well-suited for building tool-backed agentic systems that stream results via SSE or 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
- Support for automated unit and integration testing
- Emphasis on configurability
- Thorough, high-quality documentation
- 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)
- Traditional HTTP streaming
- 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).
Maven
<dependency>
<groupId>com.soklet</groupId>
<artifactId>soklet</artifactId>
<version>2.1.3</version>
</dependency>
Gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'com.soklet:soklet:2.1.3'
}
Direct Download
If you don't use Maven or Gradle, you can drop soklet-2.1.3.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-2.1.3.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-2.1.3.jar -processor com.soklet.SokletProcessor -d build src/com/soklet/example/App.java
Run
java -cp soklet-2.1.3.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 theSokletProcessor annotation processor, avoiding
classpath scans at startup. See the Request Handling docs for details.
Access To Request Data
Resource Methods (ResourceMethod) can accept aRequest parameter and inspectHttpMethod 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");
// 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();
// 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 aValueConverterRegistry populated withValueConverter<F,T>.
Conversions are applied to parameters annotated with@QueryParameter,@PathParameter,@RequestHeader,@RequestCookie,@FormParameter, and@Multipart.
Supply your own registry (or additional converters) viaSokletConfig 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 andThrowableHandler) 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();
Already know exactly what bytes you want to send over the wire? Use MarshaledResponse to skip additional processing.
@GET("/example-image.png")
public MarshaledResponse exampleImage() throws IOException {
Path imageFile = Path.of("/home/user/test.png");
byte[] image = Files.readAllBytes(imageFile);
// Serve "final" bytes over the wire
return MarshaledResponse.withStatusCode(200)
.headers(Map.of(
"Content-Type", Set.of("image/png"),
"Content-Length", Set.of(String.valueOf(image.length))
))
.body(image)
.build();
}
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 dedicatedSseServer, and a dedicatedMcpServer.
These builders let you configure host, timeouts, handler concurrency/queueing, request size limits, and connection caps; you
can also plug in custom IdGenerator,McpSessionStore, andMultipartParser instances.
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 aSseHandshakeResult, served from a dedicatedSseServer 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 dedicatedMcpServer 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 withMcpCorsAuthorizer:
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 MCPPOST / GET / DELETE responses for the configured origins. The defaultnonBrowserClientsOnlyInstance() remains conservative and keeps browser CORS disabled.
Soklet's MCP v1 support is intentionally conservative: single-request JSON-RPC only, framework-generatedtools/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 andMultipartField.
@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> instanceClass) {
// Have Soklet ask the Guice Injector for the instance
return injector.getInstance(instanceClass);
}
}).build();
Now, your Resources are dependency-injected just like the rest of your application is:
public class WidgetResource {
private WidgetService widgetService;
@Inject
public WidgetResource(WidgetService widgetService) {
this.widgetService = widgetService;
}
@GET("/widgets")
public List<Widget> widgets() {
return widgetService.findWidgets();
}
}
Lifecycle Handling and Interception
Implement LifecycleObserver andRequestInterceptor to hook into server and request lifecycles.
HTTP Server Start/Stop: execute code immediately before and after HttpServer startup and shutdown.
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willStartHttpServer(@NonNull HttpServer httpServer) {
// Perform startup tasks required prior to server launch
MyPayrollSystem.INSTANCE.startLengthyWarmupProcess();
}
@Override
public void didStartHttpServer(@NonNull HttpServer httpServer) {
// HTTP server has fully started up and is listening
System.out.println("HTTP server started.");
}
@Override
public void willStopHttpServer(@NonNull HttpServer httpServer) {
// Perform shutdown tasks required prior to server teardown
MyPayrollSystem.INSTANCE.destroy();
}
@Override
public void didStopHttpServer(@NonNull HttpServer httpServer) {
// HTTP server has fully shut down
System.out.println("HTTP server stopped.");
}
}).build();
Request Handling: these methods are fired at the very start of Request processing and the very end, respectively.
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void didStartRequestHandling(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod
) {
System.out.printf("Received request: %s\n", request);
// If there was no resourceMethod matching the request, expect a 404
if(resourceMethod != null)
System.out.printf("Request to be handled by: %s\n", resourceMethod);
else
System.out.println("This will be a 404.");
}
@Override
public void didFinishRequestHandling(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse,
@NonNull Duration processingDuration,
@NonNull List<Throwable> throwables
) {
// We have access to a few things here...
// * marshaledResponse is what was ultimately sent
// over the wire
// * processingDuration is how long everything took,
// including sending the response to the client
// * throwables is the ordered list of exceptions
// thrown during execution (if any)
long millis = processingDuration.toMillis();
System.out.printf("Entire request took %dms\n", millis);
}
}).build();
Request Wrapping: wraps around the whole "outside" of an entire Request handling flow.
Request wrapping runs before Soklet resolves which ResourceMethod should handle the request. If you want to rewrite the HTTP method or path, return a modified Request 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.
// Special scoped value so anyone can access the current Locale.
// For Java < 21, use ThreadLocal instead
public static final ScopedValue<Locale> CURRENT_LOCALE;
// Spin up the ScopedValue (or ThreadLocal)
static {
CURRENT_LOCALE = ScopedValue.newInstance();
}
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).requestInterceptor(new RequestInterceptor() {
@Override
public void wrapRequest(
@NonNull ServerType serverType,
@NonNull Request request,
@NonNull Consumer<Request> requestProcessor
) {
// Make the locale accessible by other code during this request...
Locale locale = request.getLocales().get(0);
// ...by binding it to a ScopedValue (or ThreadLocal).
ScopedValue.where(CURRENT_LOCALE, locale).run(() -> {
// You must call this so downstream processing can proceed
requestProcessor.accept(request);
});
}
}).build();
// Then, elsewhere in your code while a request is being processed:
class ExampleService {
void accessCurrentLocale() {
// You now have access to the Locale bound to the logical scope
// (or Thread) without having to pass it down the call stack
Locale locale = CURRENT_LOCALE.orElse(Locale.getDefault());
}
}
Request Intercepting (via RequestInterceptor): provides programmatic control over two processing steps.
- Invoking the appropriate
ResourceMethodto acquire aMarshaledResponse - Sending the
MarshaledResponseover the wire to the client
You must call responseWriter.accept(...) exactly once before returning; otherwise Soklet logs an error and returns a 500 response.
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).requestInterceptor(new RequestInterceptor() {
@Override
public void interceptRequest(
@NonNull ServerType serverType,
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull Function<Request, MarshaledResponse> responseGenerator,
@NonNull Consumer<MarshaledResponse> responseWriter
) {
// Here's where you might start a DB transaction.
// (MyDatabase is a hypothetical construct)
MyDatabase.INSTANCE.beginTransaction();
// Step 1: Invoke the Resource Method and acquire its response
MarshaledResponse response = responseGenerator.apply(request);
// Commit the DB transaction before sending the response
// to reduce contention by keeping "open" time short
MyDatabase.INSTANCE.commitTransaction();
// Set a special header on the response via mutable copy
response = response.copy().headers((mutableHeaders) -> {
mutableHeaders.put("X-Powered-By", Set.of("Soklet"));
}).finish();
// Step 2: Send the finalized response over the wire
responseWriter.accept(response);
}
}).build();
Response Writing: monitor the response writing process for each MarshaledResponse - sending bytes over the wire - which may terminate exceptionally (e.g. unexpected client disconnect).
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
@Override
public void willStartResponseWriting(
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse
) {
// Access to marshaledResponse here lets us see exactly
// what will be going over the wire
byte[] body = marshaledResponse.getBody().orElse(new byte[] {});
System.out.printf("About to start writing response with " +
"a %d-byte body...\n", body.length);
}
@Override
public void didFinishResponseWriting(
@NonNull Request request,
@Nullable ResourceMethod resourceMethod,
@NonNull MarshaledResponse marshaledResponse,
@NonNull Duration responseWriteDuration,
@Nullable Throwable throwable
) {
long millis = responseWriteDuration.toMillis();
System.out.printf("Took %dms to write response\n", millis);
// You have access to the throwable that might have occurred
// while writing the response. This is useful to, for example,
// determine trends in unexpected client disconnect rates
if(throwable != null) {
System.err.println("Exception occurred while writing response");
throwable.printStackTrace();
}
}
}).build();
CORS Support
CORS is handled by CorsAuthorizer usingCors metadata and returnsCorsPreflightResponse /CorsResponse as needed.
Authorize All Origins:
SokletConfig config = SokletConfig.withHttpServer(server)
// "Wildcard" (*) CORS authorization. Don't use this in production!
.corsAuthorizer(CorsAuthorizer.acceptAllInstance())
.build();
Authorize Whitelisted Origins:
Set<String> allowedOrigins = Set.of("https://www.revetware.com");
SokletConfig config = SokletConfig.withHttpServer(server)
.corsAuthorizer(WhitelistedOriginsCorsAuthorizer.fromOrigins(allowedOrigins))
.build();
...or be dynamic:
SokletConfig config = SokletConfig.withHttpServer(server)
.corsAuthorizer(WhitelistedOriginsCorsAuthorizer.fromAuthorizer(
(origin) -> origin.equals("https://www.revetware.com")
))
.build();
Custom CORS logic:
SokletConfig config = SokletConfig.withHttpServer(server)
.corsAuthorizer(new CorsAuthorizer() {
// Any subdomain under soklet.com is permitted
boolean originMatchesValidSubdomain(@NonNull Cors cors) {
return cors.getOrigin().matches("https://(.+)\\.soklet\\.com");
}
@NonNull
@Override
public Optional<CorsPreflightResponse> authorizePreflight(
@NonNull Request request,
@NonNull Map<HttpMethod, ResourceMethod> availableResourceMethodsByHttpMethod
) {
// Requests here are guaranteed to have the Cors value set
Cors cors = request.getCors().orElseThrow();
// Only greenlight our soklet.com subdomains
if (originMatchesValidSubdomain(cors))
return Optional.of(
CorsPreflightResponse.withAccessControlAllowOrigin(cors.getOrigin())
.accessControlAllowMethods(availableResourceMethodsByHttpMethod.keySet())
.accessControlAllowHeaders(Set.of("*"))
.accessControlAllowCredentials(true)
.accessControlMaxAge(Duration.ofMinutes(10))
.build()
);
return Optional.empty();
}
@NonNull
@Override
public Optional<CorsResponse> authorize(@NonNull Request request) {
// Requests here are guaranteed to have the Cors value set
Cors cors = request.getCors().orElseThrow();
// Only greenlight our soklet.com subdomains
if (originMatchesValidSubdomain(cors))
return Optional.of(
CorsResponse.withAccessControlAllowOrigin(cors.getOrigin())
.accessControlExposeHeaders(Set.of("*"))
.build()
);
return Optional.empty();
}
})
.build();
Unit Testing
First, define something to test:
public class ReverseResource {
// Reverse the input
@POST("/reverse")
public List<Integer> reverse(@RequestBody List<Integer> numbers) {
return numbers.reversed();
}
// Reverse the input and set custom headers/cookies
@POST("/reverse-again")
public Response reverseAgain(@RequestBody List<Integer> numbers) {
Integer largest = Collections.max(numbers);
Instant lastRequest = Instant.now();
return Response.withStatusCode(200)
.headers(Map.of("X-Largest", Set.of(String.valueOf(largest))))
.cookies(Set.of(
ResponseCookie.with("lastRequest", lastRequest.toString()).build()
))
.body(numbers.reversed())
.build();
}
}
Perform tests:
import org.junit.Assert;
import org.junit.Test;
@Test
public void reverseUnitTest() {
// Your Resource is a Plain Old Java Object, no Soklet dependency
ReverseResource resource = new ReverseResource();
List<Integer> input = List.of(1, 2, 3);
List<Integer> expected = List.of(3, 2, 1);
List<Integer> actual = resource.reverse(input);
Assert.assertEquals("Reverse failed", expected, actual);
}
@Test
public void reverseAgainUnitTest() {
ReverseResource resource = new ReverseResource();
List<Integer> input = List.of(1, 2, 3);
// Set expectations
List<Integer> expectedBody = List.of(3, 2, 1);
Integer expectedCode = 200;
Integer expectedLargest = Collections.max(input);
Instant lastRequestAfter = Instant.now();
Response response = resource.reverseAgain(input);
// Extract actuals
Integer actualCode = response.getStatusCode();
List<Integer> actualBody = (List<Integer>) response.getBody().orElseThrow();
Integer actualLargest = response.getHeaders().get("X-Largest").stream()
.findAny()
.map(value -> Integer.valueOf(value))
.orElseThrow();
Instant actualLastRequest = response.getCookies().stream()
.filter(responseCookie -> responseCookie.getName().equals("lastRequest"))
.findAny()
.map(responseCookie -> Instant.parse(responseCookie.getValue().orElseThrow()))
.orElseThrow();
// Verify expectations vs. actuals
Assert.assertEquals("Bad status code", expectedCode, actualCode);
Assert.assertEquals("Reverse failed", expectedBody, actualBody);
Assert.assertEquals("Largest header failed", expectedLargest, actualLargest);
Assert.assertTrue("Last request too early", actualLastRequest.isAfter(lastRequestAfter));
}
Integration Testing
First, define something to test:
public class HelloResource {
// Hypothetical service that performs business logic
private HelloService helloService;
public HelloResource(HelloService helloService) {
this.helloService = helloService;
}
// Respond with a 'hello' message, e.g. Hello, Mark
@GET("/hello")
public String hello(@QueryParameter String name) {
return this.helloService.sayHelloTo(name);
}
}
Perform tests:
Soklet's Simulator is available via Soklet to exercise full request/response flows without binding a port.
@Test
public void basicIntegrationTest() {
// Just use your app's existing configuration
SokletConfig config = obtainMySokletConfig();
// Instead of running in a real HTTP server that listens on a port,
// a simulator is provided against which you can issue requests
// and receive responses.
Soklet.runSimulator(config, (simulator -> {
// Construct a request
Request request = Request.withPath(HttpMethod.GET, "/hello")
.queryParameters(Map.of("name", Set.of("Mark")))
.build();
// Perform the request and get a handle to the response
HttpRequestResult httpRequestResult = simulator.performHttpRequest(request);
MarshaledResponse marshaledResponse = httpRequestResult.getMarshaledResponse();
// Verify status code
Integer expectedCode = 200;
Integer actualCode = marshaledResponse.getStatusCode();
Assert.assertEquals("Bad status code", expectedCode, actualCode);
// Verify response body
marshaledResponse.getBody().ifPresentOrElse(body -> {
String expectedBody = "Hello, Mark";
String actualBody = new String(body, StandardCharsets.UTF_8);
Assert.assertEquals("Bad response body", expectedBody, actualBody);
}, () -> {
Assert.fail("No response body");
});
}));
}
Metrics Collection
Soklet includes a MetricsCollector hook for collecting HTTP and SSE telemetry. The default in-memory
collector is enabled automatically, but you can replace or disable it:
SokletConfig config = SokletConfig.withHttpServer(
HttpServer.fromPort(8080)
).metricsCollector(
MetricsCollector.defaultInstance()
// or MetricsCollector.disabledInstance()
).build();
Use MetricsCollector.SnapshotTextOptions andMetricsCollector.MetricsFormat to control text output.
You can expose a /metrics endpoint by injecting MetricsCollector
into a ResourceMethod:
@GET("/metrics")
public MarshaledResponse getMetrics(@NonNull MetricsCollector metricsCollector) {
SnapshotTextOptions options = SnapshotTextOptions
.withMetricsFormat(MetricsFormat.PROMETHEUS)
.histogramFormat(HistogramFormat.FULL_BUCKETS)
.includeZeroBuckets(false)
.build();
String body = metricsCollector.snapshotText(options).orElse(null);
if (body == null)
return MarshaledResponse.fromStatusCode(204);
return MarshaledResponse.withStatusCode(200)
.headers(Map.of("Content-Type", Set.of("text/plain; charset=UTF-8")))
.body(body.getBytes(StandardCharsets.UTF_8))
.build();
}
Servlet Integration
Optional support is available for both legacy javax.servlet and current jakarta.servlet specifications. Just add the appropriate JAR to your project and you're good to go.
The Soklet website has in-depth Servlet integration documentation.
Learning More
Please refer to the official Soklet website https://www.soklet.com for detailed documentation.
Credits
Soklet stands on the shoulders of giants. Internally, it embeds code from the following OSS projects:
- Microhttp by Elliot Barlas - MIT License
- Selenium - Apache 2.0 License
- Apache Commons FileUpload - Apache 2.0 License
- The Spring Framework - Apache 2.0 License
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi