HTTP Response Handling
Component return values are automatically converted to HTTP responses. This page covers all response types and how to handle errors.
JSON Response
Most component return values are serialized as JSON with a 200 OK status:
from {
status = 'success'
data = ['item1', 'item2']
}Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"data": ["item1", "item2"]
}Arrays, records, numbers, text, and booleans are all serialized to their JSON equivalents.
No Content Response
Return nothing to send a 204 No Content response:
param orderId: text
let _ = deleteOrder orderId
from nothingResponse:
HTTP/1.1 204 No ContentThis is useful for fire-and-forget operations where no response body is needed.
Stream Response
Components can return streams for file downloads or large responses:
param fileId: text
from files.read($'documents/{fileId}')The stream's MIME type is automatically set as the Content-Type header, and the content is streamed to the client without buffering.
This is efficient for:
- File downloads
- Large data exports
- Binary content
Raw HTTP Response
For full control over status code, headers, and body, return a record with a numeric statusCode field. The presence of this field is what signals "this is a raw response":
from {
statusCode = 302
headers = {
'Location' -> '/login'
}
}Response:
HTTP/1.1 302 Found
Location: /loginThe shape of the response record is:
{
statusCode: number
headers?: { text -> text | [text] }
content?: value
}Status Code
Any HTTP status code is supported:
// Created
from { statusCode = 201, content = { id = newid } }
// Custom error with details
from {
statusCode = 422
content = {
error = {
code = 'VALIDATION_FAILED'
details = { field = 'email' }
}
}
}
// I'm a teapot
from { statusCode = 418 }Headers
A header value can be a single text (the common case) or a list of text for multi-value headers like Set-Cookie:
from {
statusCode = 200
headers = {
'Cache-Control' -> 'public, max-age=3600'
'X-Trace-Id' -> 'abc-123'
'Set-Cookie' -> [
'session=abc; HttpOnly'
'theme=dark'
]
}
}If you set Content-Type via headers, it overrides the default that would otherwise be inferred from the content:
from {
statusCode = 200
headers = {
'Content-Type' -> 'application/xml'
}
content = '<?xml version="1.0"?><ok/>'
}Content
The content field can be any value:
content type | Behavior |
|---|---|
omitted / nothing | Empty body |
text | Written as-is, default Content-Type: text/plain; charset=utf-8 |
| stream | Written as binary, default Content-Type from stream's MIME type |
| record/list/number/etc. | JSON-serialized, default Content-Type: application/json |
// Plain text response
from {
statusCode = 200
content = 'Hello, world!'
}
// Custom JSON shape with custom status
from {
statusCode = 202
headers = { 'Location' -> '/jobs/123/status' }
content = { jobId = '123', status = 'queued' }
}When to use raw responses
Use the raw response model when you need to:
- Set a non-default HTTP status code (redirects, custom errors, 2xx variants)
- Add custom response headers (
Location,Cache-Control,Set-Cookie, ...) - Return a non-JSON body with a custom
Content-Type
For everything else, prefer the simpler "just return a value" or "return an error record" patterns shown above.
Error Response
Return a record matching the Error type to indicate failure:
param orderId: text | nothing
from orderId match
nothing |> {
error = {
code = 'MISSING_PARAMETER'
message = 'Order ID is required'
}
}
id |> processOrder(id)Error type structure:
{
error: {
code: string
message?: string
}
}Response:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"code": "MISSING_PARAMETER",
"message": "Order ID is required"
}
}Error Status Codes
The HTTP status code is determined by the error code:
| Error Code Pattern | HTTP Status |
|---|---|
NOT_FOUND | 404 |
UNAUTHORIZED | 401 |
FORBIDDEN | 403 |
| Other codes | 400 |
Error Handling Best Practices
Provide meaningful error messages:
param productId: text
let product = query skus(skuNumber) where skuNumber == productId first
from product match
nothing |> {
error = {
code = 'NOT_FOUND'
message = $'Product {productId} not found'
}
}
p |> pValidate input early:
param email: text | nothing
param name: text | nothing
from { email, name } match
{ email: text, name: text } |> createUser(email, name)
|> {
error = {
code = 'INVALID_INPUT'
message = 'Email and name are required'
}
}HTTP Status Codes
| Status | Condition |
|---|---|
| 200 | Successful response with body |
| 204 | Successful response, no content |
| 400 | Invalid request or general error |
| 401 | Authentication required |
| 403 | Forbidden (authorization failed) |
| 404 | Resource not found |
| 405 | Wrong HTTP method |
| 500 | Internal server error |
Response Headers
Response headers are automatically set based on the content type:
| Content Type | Header Value |
|---|---|
| JSON data | application/json |
| Stream | Stream's MIME type |
| SSE | text/event-stream |
Custom response headers are not configurable through ingress properties — use the Raw HTTP Response model when you need to set them.
Large Responses
For large responses, consider:
- Pagination: Return data in pages with cursor-based pagination
- Streaming: Use iterators for lazy evaluation
- SSE: For real-time updates, use Server-Sent Events
Graph queries automatically use cursor-based pagination:
param cursor: text | nothing
param pageSize: number = 100
from query orders(orderNumber, status, createdAt)
where status == 'pending'
pageSize pageSize
cursor cursorSee Also
- HTTP Ingresses - Overview and configuration
- Parameter Mapping - Headers, query params, and body handling
- SSE Streaming - Real-time server-sent events