Skip to content

HTTP Ingresses

HTTP ingresses expose reactor components as HTTP endpoints, allowing external systems to call component methods via standard HTTP requests.

Configuration

An HTTP ingress is configured through the properties field when creating an ingress with type: http. The properties define how HTTP requests map to component parameters.

Basic Structure

uri: /resources/ingresses/my-ingress
spec:
type: http
componentId: my-component.hreactor
properties:
route: api/my-endpoint
httpMethod: post
isPublic: false
headers: {}
queryParams: {}
body:
mode: structured

Route Configuration

Route

The route property defines the URL path where the ingress will be accessible:

properties:
route: api/orders/create

The ingress will be callable at /ingress/api/orders/create.

Route Parameters

Routes can include dynamic parameters using the {paramName} syntax:

properties:
route: api/orders/{orderId}/status

Route parameters are automatically extracted and passed to the component:

Terminal window
curl -X GET https://your-instance.hantera.io/ingress/api/orders/12345/status
# orderId parameter will be "12345"

Multiple route parameters are supported:

properties:
route: api/warehouses/{warehouseId}/products/{productId}

HTTP Method

The httpMethod property defines which HTTP method the ingress accepts:

properties:
httpMethod: post # get, post, put, patch, or delete

Available methods:

  • get - HTTP GET requests
  • post - HTTP POST requests
  • put - HTTP PUT requests
  • patch - HTTP PATCH requests
  • delete - HTTP DELETE requests

Requests using other HTTP methods will receive a 405 Method Not Allowed response.

Parameter Mapping

HTTP ingresses map various parts of the HTTP request to component parameters. The mapping happens in a specific order, with later mappings potentially overriding earlier ones.

Parameter Resolution Order

  1. Route parameters - Extracted from the URL path
  2. Headers - Mapped HTTP headers
  3. Query parameters - URL query string values
  4. Body - Request body content

This order allows intentional parameter overloading. For example, a query parameter can override a route parameter if needed.

Headers

Map HTTP headers to component parameters:

properties:
headers:
userId: X-User-Id
apiKey: X-API-Key

The component will receive userId and apiKey parameters with values from the X-User-Id and X-API-Key headers.

If multiple values exist for a header, they are joined with semicolons.

Query Parameters

Map URL query string parameters to component parameters:

properties:
queryParams:
pageSize: limit
pageNumber: offset

Example request:

Terminal window
curl "https://your-instance.hantera.io/ingress/api/products?limit=50&offset=0"
# Component receives: pageSize=50, pageNumber=0

If multiple values exist for a query parameter, they are joined with semicolons.

Body Handling

The body configuration determines how the HTTP request body is processed.

Structured Mode

In structured mode, the request body must be JSON and is parsed into component parameters:

properties:
body:
mode: structured

Without a parameter name, each JSON property becomes a component parameter:

Terminal window
curl -X POST https://your-instance.hantera.io/ingress/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": "CUST001",
"items": [{"sku": "PROD001", "quantity": 2}]
}'
# Component receives: customerId="CUST001", items=[...]

With a parameter name, the entire JSON body becomes one parameter:

properties:
body:
mode: structured
parameter: orderData
Terminal window
curl -X POST https://your-instance.hantera.io/ingress/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": "CUST001",
"items": [{"sku": "PROD001", "quantity": 2}]
}'
# Component receives: orderData={customerId: "CUST001", items: [...]}

Structured mode requires Content-Type: application/json. Other content types will result in a 400 Bad Request.

Raw Mode

In raw mode, the request body is passed as a stream to the component:

properties:
body:
mode: raw
parameter: fileData

This is useful for:

  • Large structured data imports (XML, CSV)
  • Binary data processing
  • File uploads
  • Custom content types

The component receives the raw stream as a byte array ([byte]) that can be parsed as needed.

import 'xml'
param xmlData: [byte]
// Parse XML stream and extract product data
let products =
xmlData
|> xml.readXml
|> xml.whereElement('product')
select product => {
skuNumber = product |> xml.whereElement('sku') |> xml.getText
name = product |> xml.whereElement('name') |> xml.getText
price = product |> xml.whereElement('price') |> xml.getText
}
// Create SKUs by sending messages to the sku actor
let results = products select p => messageActor(
'sku'
'create'
[{
type = 'create'
body = {
skuNumber = p.skuNumber
commands = [{
type = 'setDynamicFields'
fields = {
name = p.name
price = p.price
}
}]
}
}]
)
from results

Complete Example: SKU Search API

Here’s a comprehensive example showing various HTTP ingress features:

# Component that searches SKUs
uri: /resources/components/sku-search.hreactor
spec:
codeFile: sku-search.hreactor
---
# HTTP ingress with query params
uri: /resources/ingresses/api/skus/search
spec:
type: http
componentId: sku-search.hreactor
acl:
- skus:read
properties:
route: api/skus/search
httpMethod: get
queryParams:
searchTerm: q
sortBy: sort

Component code:

sku-search.hreactor
param searchTerm: text
param sortBy: text
let sort = sortBy match
nothing |> 'skuNumber'
|> sortBy
from query skus(skuNumber)
phrase searchTerm
orderBy $'{sort} asc'

Example request:

Terminal window
curl -X GET \
"https://your-instance.hantera.io/ingress/api/skus/search?q=laptop&sort=skuNumber" \
-H "Authorization: Bearer YOUR_TOKEN"

The component receives:

  • searchTerm: “laptop” (from query)
  • sortBy: “skuNumber” (from query)

The query uses cursor-based pagination automatically, so results are returned lazily as the caller iterates through them.

Access Control

Calling HTTP Ingresses

To call an HTTP ingress, a session must have the appropriate permission based on the ingress resource ID:

ingresses[/<ingressId>]:http

Important: The permission uses the ingress resource ID (the uri when creating the ingress), NOT the HTTP route. These can be different.

Example:

uri: /resources/ingresses/api/skus/search # This is the ingressId
spec:
type: http
properties:
route: search/skus # This is the HTTP route (can be different!)

Permissions needed:

  • To call this ingress: ingresses/api/skus/search:http (uses the ingressId)
  • To manage this ingress: ingresses:read or ingresses:write

HTTP endpoint:

  • The ingress is called at: /ingress/search/skus (uses the route property)

The permission grants the ability to call the ingress, not to view or modify its configuration.

Note: Public ingresses (see below) bypass this authentication requirement entirely.

Public Ingresses

Mark an ingress as public to allow unauthenticated access:

uri: /resources/ingresses/webhooks/stripe
spec:
type: http
componentId: stripe-webhook.hreactor
properties:
route: webhooks/stripe
httpMethod: post
isPublic: true
body:
mode: raw
parameter: payload

Public ingresses:

  • Don’t require authentication
  • Have a 10 MB maximum payload size
  • Rate limiting applied by the platform
  • Should validate input carefully in component code
  • Are ideal for webhooks and public APIs

Response Handling

Component return values are automatically converted to HTTP responses:

JSON Response

Most component return values are serialized as JSON:

from {
status = 'success'
data = ['item1', 'item2']
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"data": ["item1", "item2"]
}

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.

Error Response

Return a record matching the Error type to indicate failure. HTTP ingresses automatically convert records with this structure to HTTP error responses:

param orderId: text | nothing
from orderId match
nothing |> {
error = {
code = 'MISSING_PARAMETER'
message = 'Order ID is required'
}
}
id |> processOrderInternal(id)

Error type structure: { error: { code: text, message: text | nothing } }

The HTTP response will be:

HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"code": "MISSING_PARAMETER",
"message": "Order ID is required"
}
}

Best Practices

Validation

Always validate input parameters, especially for public ingresses:

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'
}
}

Parameter Naming

Use clear, consistent parameter names:

  • customerId, orderId, productSku
  • id, val, x

HTTP Methods

Use appropriate HTTP methods:

  • GET: Retrieve data (no side effects)
  • POST: Create new resources
  • PUT: Update/replace entire resources
  • PATCH: Partial updates
  • DELETE: Remove resources

Error Handling

Provide meaningful error messages:

param productId: text
let product = query skus(skuNumber) first
from product match
nothing |> {
error = {
code = 'NOT_FOUND'
message = $'SKU not found'
}
}
p |> p

Route Design

  • Use hierarchical routes: api/orders/{orderId}/items
  • Use plural nouns for collections: api/products, not api/product
  • Keep routes simple and predictable
  • Version your API if needed: api/v1/orders

Security Considerations

Authentication

Non-public ingresses require authentication via Bearer token:

Terminal window
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://your-instance.hantera.io/ingress/api/orders

Authorization

Use ingress ACLs to control what operations the component can perform:

spec:
acl:
- orders:read
- customers:read

Input Validation

Always validate and sanitize input, especially from public ingresses:

param payload: any
param signature: text
// Validate webhook signature
let isValid = validateSignature(payload, signature)
from isValid match
false |> {
error = {
code = 'UNAUTHORIZED'
message = 'Invalid signature'
}
}
true |> processWebhook(payload)

See Also