Overview
HTTP handlers allow you to:- Implement REST APIs with custom business logic
- Create custom endpoints for monitoring, administration, or webhooks
- Process non-standard requests that don’t fit the search or feed APIs
- Integrate external systems through HTTP interfaces
- Build specialized protocols on top of HTTP
Handler Types
Vespa provides two main base classes for HTTP handlers:ThreadedHttpRequestHandler
For synchronous request processing with automatic thread management:import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import java.io.IOException;
import java.io.OutputStream;
public class MyHandler extends ThreadedHttpRequestHandler {
@Override
public HttpResponse handle(HttpRequest request) {
// Process request and return response
return new JsonResponse(200, "{\"status\":\"ok\"}");
}
private static class JsonResponse extends HttpResponse {
private final String json;
JsonResponse(int status, String json) {
super(status);
this.json = json;
}
@Override
public void render(OutputStream output) throws IOException {
output.write(json.getBytes());
}
@Override
public String getContentType() {
return "application/json";
}
}
}
ThreadedRequestHandler
For lower-level request handling with full control over request/response lifecycle:import com.yahoo.jdisc.handler.ThreadedRequestHandler;
import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.handler.BufferedContentChannel;
import com.yahoo.jdisc.Request;
import com.yahoo.jdisc.Response;
import java.util.concurrent.Executor;
public class LowLevelHandler extends ThreadedRequestHandler {
public LowLevelHandler(Executor executor) {
super(executor);
}
@Override
protected void handleRequest(Request request,
BufferedContentChannel requestContent,
ResponseHandler responseHandler) {
Response response = new Response(200);
responseHandler.handleResponse(response);
}
}
Basic Handler Structure
A simple handler returning JSON:package com.example;
import com.yahoo.component.annotation.Inject;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executor;
public class HelloHandler extends ThreadedHttpRequestHandler {
@Inject
public HelloHandler(Executor executor) {
super(executor);
}
@Override
public HttpResponse handle(HttpRequest request) {
String name = request.getProperty("name");
if (name == null) {
name = "World";
}
return new StringResponse(200,
String.format("{\"message\":\"Hello, %s!\"}", name));
}
}
Request Processing
Reading Request Data
@Override
public HttpResponse handle(HttpRequest request) {
// Get query parameters
String param = request.getProperty("param");
// With default value
String value = request.getProperty("key", "default");
// All parameters
Map<String, String> params = request.propertyMap();
return new MyResponse(params);
}
URI and Path Handling
@Override
public HttpResponse handle(HttpRequest request) {
// Get full URI
URI uri = request.getUri();
// Get path
String path = uri.getPath();
// Parse path segments
String[] segments = path.split("/");
if (segments.length > 2) {
String resourceType = segments[1];
String resourceId = segments[2];
return handleResource(resourceType, resourceId);
}
return new ErrorResponse(404, "Not found");
}
Response Generation
Custom Response Classes
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class JsonResponse extends HttpResponse {
private final String jsonContent;
public JsonResponse(int status, String jsonContent) {
super(status);
this.jsonContent = jsonContent;
}
@Override
public void render(OutputStream output) throws IOException {
output.write(jsonContent.getBytes(StandardCharsets.UTF_8));
}
@Override
public String getContentType() {
return "application/json; charset=utf-8";
}
}
public class ErrorResponse extends JsonResponse {
public ErrorResponse(int status, String message) {
super(status,
String.format("{\"error\":\"%s\",\"status\":%d}",
message, status));
}
}
Setting Response Headers
@Override
public HttpResponse handle(HttpRequest request) {
MyResponse response = new MyResponse();
// Set custom headers
response.headers().put("X-Custom-Header", "value");
response.headers().put("Cache-Control", "no-cache");
response.headers().put("Access-Control-Allow-Origin", "*");
return response;
}
Streaming Responses
import java.io.OutputStream;
public class StreamingResponse extends HttpResponse {
private final DataSource dataSource;
public StreamingResponse(DataSource source) {
super(200);
this.dataSource = source;
}
@Override
public void render(OutputStream output) throws IOException {
// Stream data in chunks
byte[] buffer = new byte[8192];
int bytesRead;
try (var stream = dataSource.openStream()) {
while ((bytesRead = stream.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
output.flush();
}
}
}
@Override
public String getContentType() {
return "application/octet-stream";
}
}
Common Handler Patterns
REST API Handler
import com.fasterxml.jackson.databind.ObjectMapper;
public class RestApiHandler extends ThreadedHttpRequestHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
private final DataService dataService;
@Inject
public RestApiHandler(Executor executor, DataService dataService) {
super(executor);
this.dataService = dataService;
}
@Override
public HttpResponse handle(HttpRequest request) {
String method = request.getMethod().name();
String path = request.getUri().getPath();
try {
return switch (method) {
case "GET" -> handleGet(path, request);
case "POST" -> handlePost(path, request);
case "PUT" -> handlePut(path, request);
case "DELETE" -> handleDelete(path, request);
default -> methodNotAllowed();
};
} catch (Exception e) {
log.log(Level.SEVERE, "Request processing failed", e);
return new ErrorResponse(500, "Internal server error");
}
}
private HttpResponse handleGet(String path, HttpRequest request) {
String id = extractId(path);
var data = dataService.get(id);
if (data == null) {
return new ErrorResponse(404, "Not found");
}
return new JsonResponse(200, toJson(data));
}
private HttpResponse handlePost(String path, HttpRequest request)
throws IOException {
String body = new String(
request.getData().readAllBytes(),
StandardCharsets.UTF_8
);
var data = objectMapper.readValue(body, MyData.class);
var created = dataService.create(data);
return new JsonResponse(201, toJson(created));
}
private String extractId(String path) {
String[] parts = path.split("/");
return parts.length > 2 ? parts[2] : null;
}
private String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Health Check Handler
public class HealthHandler extends ThreadedHttpRequestHandler {
private final List<HealthCheck> checks;
@Inject
public HealthHandler(Executor executor,
ComponentRegistry<HealthCheck> checks) {
super(executor);
this.checks = new ArrayList<>(checks.allComponents());
}
@Override
public HttpResponse handle(HttpRequest request) {
boolean allHealthy = true;
StringBuilder result = new StringBuilder("{\"checks\":[");
for (int i = 0; i < checks.size(); i++) {
HealthCheck check = checks.get(i);
boolean healthy = check.isHealthy();
allHealthy &= healthy;
if (i > 0) result.append(",");
result.append(String.format(
"{\"name\":\"%s\",\"healthy\":%b}",
check.getName(), healthy
));
}
result.append("],\"status\":\"");
result.append(allHealthy ? "healthy" : "unhealthy");
result.append("\"}");
int status = allHealthy ? 200 : 503;
return new JsonResponse(status, result.toString());
}
}
Real-World Example: VipStatusHandler
From~/workspace/source/container-disc/src/main/java/com/yahoo/container/handler/VipStatusHandler.java:31:
import com.yahoo.container.core.VipStatusConfig;
import com.yahoo.jdisc.Metric;
public final class VipStatusHandler extends ThreadedHttpRequestHandler {
private final boolean accessDisk;
private final File statusFile;
private final VipStatus vipStatus;
@Inject
public VipStatusHandler(VipStatusConfig vipConfig,
Metric metric,
VipStatus vipStatus) {
// Dedicated thread pool to avoid returning errors when
// regular pool is at capacity
super(new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)),
metric);
this.accessDisk = vipConfig.accessdisk();
this.statusFile = new File(vipConfig.statusfile());
this.vipStatus = vipStatus;
}
@Override
public HttpResponse handle(HttpRequest request) {
if (metric != null) {
metric.add("jdisc.http.requests.status", 1, null);
}
return new StatusResponse();
}
class StatusResponse extends HttpResponse {
private byte[] data = null;
private File file = null;
StatusResponse() {
super(200);
if (vipStatus != null && !vipStatus.isInRotation()) {
setOutOfServiceStatus();
} else if (accessDisk) {
checkFile();
} else {
data = "<title>OK</title>\n".getBytes();
}
}
@Override
public void render(OutputStream stream) throws IOException {
if (file != null) {
Files.copy(file.toPath(), stream);
} else if (data != null) {
stream.write(data);
}
stream.close();
}
private void setOutOfServiceStatus() {
data = "No search backends available".getBytes();
setStatus(404);
}
@Override
public String getContentType() {
return file != null ? "text/html" : "text/plain";
}
}
}
Handler Configuration
Register handlers inservices.xml:
<container id="default" version="1.0">
<handler id="com.example.HelloHandler" bundle="my-bundle">
<binding>http://*/hello</binding>
<binding>http://*/hello/*</binding>
</handler>
<handler id="com.example.RestApiHandler" bundle="my-bundle">
<binding>http://*/api/v1/*</binding>
</handler>
<handler id="com.example.HealthHandler" bundle="my-bundle">
<binding>http://*/health</binding>
</handler>
</container>
Handler Initialization
With custom configuration:import com.yahoo.component.annotation.Inject;
public class ConfiguredHandler extends ThreadedHttpRequestHandler {
private final String configValue;
private final int timeout;
@Inject
public ConfiguredHandler(Executor executor, MyHandlerConfig config) {
super(executor);
this.configValue = config.value();
this.timeout = config.timeout();
setTimeout(timeout, TimeUnit.SECONDS);
}
}
Async Response Handling
For long-running operations:import com.yahoo.container.jdisc.AsyncHttpResponse;
import java.util.concurrent.CompletableFuture;
public class AsyncHandler extends ThreadedHttpRequestHandler {
private final AsyncService asyncService;
@Inject
public AsyncHandler(Executor executor, AsyncService service) {
super(executor);
this.asyncService = service;
}
@Override
public HttpResponse handle(HttpRequest request) {
return new AsyncHttpResponse(200) {
@Override
public void render(OutputStream output,
ContentChannel channel,
CompletionHandler handler) {
// Start async operation
CompletableFuture.supplyAsync(() ->
asyncService.fetchData()
).thenAccept(data -> {
try {
output.write(data.getBytes());
output.flush();
} catch (IOException e) {
log.warning("Failed to write response: " + e);
} finally {
handler.completed();
}
}).exceptionally(ex -> {
handler.failed(ex);
return null;
});
}
};
}
}
Testing Handlers
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.jdisc.http.HttpRequest.Method;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.jupiter.api.Assertions.*;
public class MyHandlerTest {
@Test
public void testHandler() throws Exception {
MyHandler handler = new MyHandler(Runnable::run);
// Create test request
HttpRequest request = HttpRequest.createTestRequest(
"http://localhost/test?param=value",
Method.GET
);
// Handle request
HttpResponse response = handler.handle(request);
// Verify response
assertEquals(200, response.getStatus());
ByteArrayOutputStream out = new ByteArrayOutputStream();
response.render(out);
String content = out.toString();
assertTrue(content.contains("expected"));
}
}
Security Considerations
- Validate all input: Never trust user input
- Use authentication: Implement proper auth for sensitive endpoints
- Rate limiting: Protect against abuse
- HTTPS: Use TLS for production deployments
Performance Tips
- Use dedicated thread pools for handlers with different characteristics
- Stream large responses instead of buffering in memory
- Set appropriate timeouts with
setTimeout() - Cache expensive computations
- Monitor handler latency and throughput
Next Steps
- Learn about Searchers for query processing
- Explore Document Processors for feed processing
- See Plugins and Bundles for packaging handlers