Skip to content

HTTP Ingress Handlers

HTTP ingress rules map an HTTP method + URL path to a JavaScript script. When a matching request arrives, DieselEngine creates a fresh GraalVM context, evaluates your script as an ES module, and calls the exported handleRequest function.

How It Works

  1. A request arrives at a URL matching an ingress rule (e.g., GET /api/items)
  2. The engine loads the linked script and creates a new GraalVM context
  3. All global bindings (diesel, Results, okHttpClient, etc.) are injected
  4. The script's handleRequest(context) export is called
  5. The returned IngressResult is sent as the HTTP response
  6. The GraalVM context is closed

INFO

Each HTTP request gets its own isolated GraalVM context. There is no shared state between requests unless you use the database, Redis, or the registry.

Ingress Types

TypeHTTP MethodUse Case
HTTP_GETGETRead operations, queries
HTTP_POSTPOSTCreate/update operations
HTTP_DELETEDELETEDelete operations

Basic Handler

Every HTTP ingress script must export a handleRequest function that receives a RequestContext and returns a Results chain:

javascript
export function handleRequest(context) {
  return Results.json().renderRaw(JSON.stringify({
    message: "Hello from DieselEngine!",
    timestamp: Date.now()
  }));
}

URL Path

All HTTP ingress routes are served behind the /backend path prefix (added by the Caddy reverse proxy in the standard Docker setup). So an ingress with route /api/items is accessible at:

http://your-host/backend/api/items

Reading Parameters

Query Parameters (GET)

javascript
export function handleRequest(context) {
  const page = context.getParameterAsInteger("page") || 1;
  const limit = context.getParameterAsInteger("limit") || 20;
  const search = context.getParameter("q") || "";

  const items = ItemService.findItems(search, page, limit);
  return Results.json().renderRaw(JSON.stringify({ data: items, page, limit }));
}

Request Body (POST)

javascript
export function handleRequest(context) {
  const body = JSON.parse(context.getBody());
  const { name, email, role } = body;

  if (!name || !email) {
    return Results.badRequest().json().renderRaw(
      JSON.stringify({ error: "name and email are required" })
    );
  }

  const id = UserService.createUser({ name, email, role });
  return Results.created("/api/users/" + id)
    .json()
    .renderRaw(JSON.stringify({ id }));
}

Path-Based Routing

DieselEngine routes are exact matches. To handle paths like /api/items/123, use query parameters (/api/items?id=123) or parse the path manually:

javascript
export function handleRequest(context) {
  const path = context.getRequestPath();
  const segments = path.split("/");
  const id = segments[segments.length - 1];
  // ...
}

The Delegation Pattern

Keep handlers thin. Extract parameters, delegate to a service class, and format the response:

javascript
import { ItemService } from '../../services/ItemService.js';

export function handleRequest(context) {
  const id = context.getParameterAsInteger("id");

  if (!id) {
    return Results.badRequest().json().renderRaw(
      JSON.stringify({ error: "Missing 'id' parameter" })
    );
  }

  const item = ItemService.getItemById(id);

  if (!item) {
    return Results.notFound().json().renderRaw(
      JSON.stringify({ error: "Item not found" })
    );
  }

  return Results.json().renderRaw(JSON.stringify(item));
}

The service class encapsulates all database access:

javascript
// /services/ItemService.js
export class ItemService {
  static getItemById(id) {
    const ps = diesel.prepareStatement(
      'SELECT id, data, created_at FROM engine."items" WHERE id = ? AND deleted = false'
    );
    ps.setLong(1, id);
    const rs = ps.executeQuery();
    if (!rs.next()) return null;
    return {
      id: rs.getLong("id"),
      ...JSON.parse(rs.getString("data")),
      created_at: rs.getString("created_at")
    };
  }
}

Auth Patterns

Bearer Token

javascript
export function handleRequest(context) {
  const auth = context.getHeader("Authorization");
  if (!auth || !auth.startsWith("Bearer ")) {
    return Results.unauthorized().json().renderRaw(
      JSON.stringify({ error: "Missing or invalid token" })
    );
  }

  const token = auth.substring(7);
  const user = AuthService.validateToken(token);
  if (!user) {
    return Results.forbidden().json().renderRaw(
      JSON.stringify({ error: "Invalid token" })
    );
  }

  // Proceed with authenticated user...
  return Results.json().renderRaw(JSON.stringify({ user }));
}
javascript
export function handleRequest(context) {
  const cookie = context.getHeader("Cookie");
  const sessionId = parseCookie(cookie, "session_id");
  // ...
}

File Uploads

Handle multipart file uploads using the file item iterator:

javascript
export function handleRequest(context) {
  const iterator = context.getFileItemIterator();

  while (iterator.hasNext()) {
    const item = iterator.next();
    if (!item.isFormField()) {
      const fieldName = item.getFieldName();
      const fileName = item.getName();
      const stream = item.openStream();
      // Upload to MinIO, process, etc.
    }
  }

  return Results.json().renderRaw(JSON.stringify({ uploaded: true }));
}

Error Handling

Script exceptions are caught by the engine. If your handler throws, the engine returns HTTP 500 and pushes the stack trace to the console. For controlled error responses, use try/catch:

javascript
export function handleRequest(context) {
  try {
    const data = JSON.parse(context.getBody());
    const result = processData(data);
    return Results.json().renderRaw(JSON.stringify(result));
  } catch (e) {
    console.error("Handler error:", e.message);
    return Results.internalServerError().json().renderRaw(
      JSON.stringify({ error: "Internal error", detail: e.message })
    );
  }
}

Response Patterns

Redirect

javascript
return Results.redirect("/dashboard");           // 303 See Other
return Results.redirectTemporary("/maintenance"); // 307 Temporary Redirect

No Content

javascript
// DELETE handler
return Results.noContent();  // 204

Custom Headers

javascript
return Results.json()
  .addHeader("X-Total-Count", String(total))
  .addHeader("Cache-Control", "public, max-age=300")
  .renderRaw(JSON.stringify(data));

Non-JSON Content

javascript
// CSV export
return Results.ok()
  .contentType("text/csv")
  .addHeader("Content-Disposition", "attachment; filename=export.csv")
  .renderRaw(csvContent);

// HTML page
return Results.html().render(htmlString);

// Plain text
return Results.text().renderRaw("OK");

Filter Scripts

Each ingress rule can optionally reference a filter script via filterScriptUUID. The filter script runs before the main handler in a separate GraalVM context. It exports handleRequest(context) — return null to allow the request through, or return an IngressResult (e.g. Results.unauthorized()) to deny. Filter scripts work with HTTP_GET, HTTP_POST, and HTTP_DELETE rules.

This is useful for cross-cutting concerns like authentication, rate limiting, or IP allowlisting that apply to multiple routes without duplicating logic in every handler.

How Filters Work

  1. A request arrives matching an ingress rule that has a filterScriptUUID set
  2. The engine creates a separate GraalVM context and runs the filter script
  3. The filter's handleRequest(context) is called
  4. If the filter returns null, the request proceeds to the main handler
  5. If the filter returns an IngressResult, that response is sent immediately and the main handler is not executed

Filter Script Example

javascript
// /filters/require-api-key.js
// Reusable filter — attach to any ingress rule via filterScriptUUID

const VALID_API_KEYS = ["key-abc-123", "key-def-456"];

export function handleRequest(context) {
  const apiKey = context.getHeader("X-API-Key");

  if (!apiKey || !VALID_API_KEYS.includes(apiKey)) {
    return Results.unauthorized().json().renderRaw(
      JSON.stringify({ error: "Invalid or missing API key" })
    );
  }

  // Pass the validated key to the main handler via context attributes
  context.setAttribute("apiKey", apiKey);

  // Return null to allow the request through
  return null;
}

Passing Data to the Main Handler

Filter scripts can attach data to the request context using context.setAttribute(). The main handler can then read it with context.getAttribute():

javascript
// Main handler — reads data set by the filter
export function handleRequest(context) {
  const apiKey = context.getAttribute("apiKey");
  // The filter already validated the key, so we can proceed safely
  return Results.json().renderRaw(JSON.stringify({ authenticated: true, key: apiKey }));
}

TIP

Filter scripts are set via the filter_script_uuid parameter on the ingress_create and ingress_edit MCP tools. A single filter script can be shared across multiple ingress rules.

DieselEngine Scripting Documentation