Outbound HTTP requests
An HttpRequest is an asynchronous outbound HTTP call, available as an action type in both triggers and processors. These actions are queued during trigger or processor execution and dispatched after the response is sent to the client. They are fire-and-forget — they do not block the response.
Quick example
Send a webhook notification when content under /blog/ is created or updated:
<div itemscope itemtype="https://pagelove.org/Trigger">
<meta itemprop="resource" content="/blog/*">
<meta itemprop="method" content="PUT">
<meta itemprop="method" content="POST">
<div itemprop="action" itemscope itemtype="https://pagelove.org/HttpRequest">
<meta itemprop="url" content="https://hooks.example.com/notify">
<meta itemprop="method" content="POST">
<meta itemprop="body" content="Blog content updated">
</div>
</div>
Properties
url (required)
Target URL for the outbound request.
<meta itemprop="url" content="https://hooks.example.com/notify">
method
HTTP method. Defaults to POST. Supported methods: GET, POST, PUT, DELETE, PATCH.
<meta itemprop="method" content="POST">
content-type
Content-Type header sent with the request. Defaults to text/html.
<meta itemprop="content-type" content="application/json">
body
Request body content.
<meta itemprop="body" content="<p>Content was updated</p>">
header
An additional request header, written as a Pair of key and value. Repeat
the property for as many headers as the request needs — this is how an outbound
request carries an API token, a tenant identifier, or anything else the service
you are calling expects.
<div itemprop="action" itemscope itemtype="https://pagelove.org/HttpRequest">
<meta itemprop="url" content="https://api.postmarkapp.com/email">
<meta itemprop="content-type" content="application/json">
<div itemprop="header" itemscope itemtype="https://pagelove.org/Pair">
<meta itemprop="key" content="X-Postmark-Server-Token">
<meta itemprop="value" content="1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d">
</div>
<div itemprop="header" itemscope itemtype="https://pagelove.org/Pair">
<meta itemprop="key" content="Accept">
<meta itemprop="value" content="application/json">
</div>
</div>
A header value can be dynamic, like any other property:
<div itemprop="header" itemscope itemtype="https://pagelove.org/Pair">
<meta itemprop="key" content="X-Request-Path">
<div itemprop="value" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Context url("https://pagelove.org/Context");
Context.request.path
</script>
</div>
</div>
A header may also be written as a single line of text, split on its first colon:
<meta itemprop="header" content="X-Api-Version: 2">
Repeats are kept. The same field name may appear more than once, and headers are sent in the order you write them.
Content-Type has one home. If a header sets Content-Type, it replaces
the content-type property rather than being sent alongside it, so the field
never appears twice.
Malformed headers are dropped. A name that is not a valid HTTP field name, or a value containing control characters such as carriage return or line feed, is discarded rather than sent — including when a dynamic value produces one. The rest of the request is unaffected.
A header value written here lives in the document. An API token in a
headeris stored in the document that declares it, and is readable by anyone your authorization rules allow to read that document. Keep documents that carry credentials off any path that grants public read access.
Static vs dynamic properties
Any property can be either a static literal or a dynamic Sessel or JavaScript expression. This makes it possible to build request URLs and bodies from request context:
<div itemprop="action" itemscope itemtype="https://pagelove.org/HttpRequest">
<!-- Static method -->
<meta itemprop="method" content="POST">
<!-- Dynamic URL built from request path (Sessel) -->
<div itemprop="url" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Context url("https://pagelove.org/Context");
"https://hooks.example.com/" + Context.request.path
</script>
</div>
<!-- Dynamic body built from request data (Sessel) -->
<div itemprop="body" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Context url("https://pagelove.org/Context");
Context.request.path
</script>
</div>
</div>
The same properties accept a https://pagelove.org/JavaScript/Module typed item instead. The language is selected per property by its itemtype, so a single HttpRequest action can mix Sessel and JavaScript across different properties:
<div itemprop="action" itemscope itemtype="https://pagelove.org/HttpRequest">
<!-- Dynamic URL built from request path (JavaScript) -->
<div itemprop="url" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script type="module" itemprop="source">
export default function(ctx) { return "/api/" + ctx.request.path; }
</script>
</div>
<meta itemprop="method" content="GET">
</div>
Retry
On failure, outbound requests retry with exponential backoff:
| Attempt | Delay before attempt |
|---|---|
| 1 | immediate |
| 2 | 2 seconds |
| 3 | 4 seconds |
| 4 | 8 seconds |
| 5 | 16 seconds |
Maximum 5 attempts. The backoff delay would be capped at 30 seconds, though the schedule above never reaches that cap. After all attempts are exhausted, the request is abandoned.
Failures do not affect the client response (already sent).
Error handling
- Transport errors (connection refused, timeout): retried according to the retry schedule.
- Non-2xx responses: retried according to the retry schedule.
- After all retries exhausted: the request is abandoned. No effect on the client.
See also
- Triggers — fire before core processing
- Processors — fire after core processing
- JavaScript in schemas — JavaScript as a Pagelove binding language
- Sessel language reference — expression language for dynamic properties