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>">

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 header is 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

See also