---
title: http
description: "Make an `http` Effect: a call to a URL, with Effect secrets that the Agent never reads."
---

# `http`

Function, in `@jr2/orchestrator/src/http-effect.ts`. It is an export of `@jr2/orchestrator`, on the [Authoring surface](../authoring-surface.md) page, in Effects.

```ts
function http<
  S extends z.ZodType | undefined = undefined,
  const U extends HttpDefinition<S>["url"] = string,
  const A extends HttpDefinition<S>["auth"] = undefined,
  const H extends HttpDefinition<S>["headers"] = undefined,
>(
  def: HttpDefinition<S> & { url: U; auth?: A; headers?: H },
): EffectLogic<HttpInput<S>, HttpOutput> & OpenOf<HttpOpenParts<U, A, H>>
```

Make an `http` Effect: a call to a URL, with Effect secrets that the Agent never reads. Put it
in `actors`, and invoke it by its key: `actors: { comment: http({ ... }) }`, then `invoke: {
src: "comment" }`.

It runs in the Instance Effector, which `jr2 up` deploys when a Machine has an `http` Effect.
With `body: file(...)`, it runs in the Sandbox Effector of the Workspace whose body invokes it.
The Effector makes the call. The Orchestrator holds no secret.

Every outcome is output (`HttpOutput`): `response`, `unreachable`, `refused` or `unknown`. A 4xx
or 5xx status is output too. jr2 sends the call at most one time, unless you mark it
`idempotent`. When its state exits while the call runs, jr2 stops waiting for it. The Effector
can still send it.

Each field of [`HttpDefinition`](type-httpdefinition.md) tells what it refuses. `createMachine` also refuses an
Effect written directly in an `invoke`: `machine "<id>": state "<state>" invokes an Effect
inline — a Part is a key of actors, so it has a name that the ledger, the report and
customize() share: actors: { <name>: http(...) }, then invoke: { src: "<name>" }`.

**Parameters**

- `def`: The definition. Each field tells what it does.

  - `method`: [`HttpMethod`](type-httpmethod.md)

    The HTTP method: `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` or `OPTIONS`. It refuses
    another: `http({ method: "<method>" }): the method is one of GET, HEAD, POST, PUT, PATCH,
    DELETE, OPTIONS`.
  - `url`: `string | Text | typeof open`

    The url: `http://` or `https://`, then a literal host. No input, param or Effect secret can
    change where the call goes. An Effect secret can be only in the path:
    `` text`https://api.telegram.org/bot${secret("tg")}/sendMessage` ``.

    Or `open`: a packaged Machine that cannot know the host leaves the url Open, and the composer
    binds it one time with `customize(machine, { effects: { <slot>: { url } } })`.

    It refuses:
    - a scheme or host that is not literal: `` http({ url }): the url is an http:// or https:// url
      whose scheme and host are literal — a secret may sit only in its path, after a "/":
      text`https://api.example.com/bot${secret("tg")}/send` (ADR-0068) ``
    - an Effect secret after `?` or `#`: `` http({ url }): secret("<name>") sits in the url's query
      or fragment — a secret may sit only in its path; send it in `auth` or a header instead:
      headers: { "x-api-key": text`${secret("...")}` } (ADR-0068) ``
    - a host that is not a host: `http({ url }): "<scheme>://<host>" is not a host`
    - a user name or password in the url: ``http({ url }): the url carries userinfo — use `auth` ``
  - `headers` (optional): `Readonly<Record<string, string | Text>>`

    Headers that are the same for each run. Each name is lowercase. A value is a string, or a
    `text` with an Effect secret.

    It refuses:
    - a name that is not lowercase, or that frames or routes the request (`host`,
      `content-length`, `transfer-encoding`, `connection`, `upgrade`, `te`, `trailer`,
      `keep-alive`, `proxy-*`): `http({ headers }): "<name>" is not a header an Effect sets — a
      lowercase name that does not frame or route the request (ADR-0068)`
    - an `authorization` header beside `auth`: ``http({ headers }): "authorization" is set by
      `auth` — write one of them``
  - `auth` (optional): `{ readonly bearer: Secret; } | { readonly basic: { user: Text; password: Text; }; } | Text`

    The `Authorization` header: `bearer(secret("x"))`, `basic(user, password)`, or a `text` that
    is the whole value.
  - `input` (optional): `S`

    The zod schema of the input of the Effect: what the `input` of the `invoke`, or `fromPick`,
    gives it. jr2 parses the input with it before the call. When it starts, input that does not
    agree with it makes the Effect fail (its `onError` route): `http "<slot>": its input does not
    match the definition's input schema — <path>: <message>`, one `<path>: <message>` for each
    problem, joined by `; `.
  - `request` (optional): `(input: S extends z.ZodType ? z.infer<S> : void) => HttpRequestParts`

    A function that makes the part of the request that changes for each run, from the input:
    `path` segments, and a `json` or `text` body. It cannot change the host, a header or an
    Effect secret. Do not write it beside `body`.
  - `body` (optional): `FileBody<S>`

    A Worktree file as the body: `file(path, { slot?, segments?, contentType? })`. The Effect then
    runs in the Sandbox Effector of its Workspace. Do not write `request` beside it: the `segments`
    of `file` are the path. It refuses another body: ``http({ body }): a body is file(path, {
    slot?, segments?, contentType? }) — json and text come from `request` ``.
  - `timeout` (optional): [`Duration`](type-duration.md)

    How long jr2 waits for the answer: a number and a unit, such as `"30s"` or `"2m"`. Default
    `"30s"`. With no answer in that time, the output is `unknown`. It refuses a value that is not
    a positive duration: `http: timeout "<timeout>" is not a positive duration (like "30s",
    "2m")`.
  - `maxBody` (optional): `number`

    The largest number of bytes of a text response body that the output keeps. Default 65536.
    When jr2 cuts the body, `truncated` is `true`. It refuses a value that is not a positive whole
    number: `http({ maxBody }): a positive number of bytes`.
  - `idempotent` (optional): `boolean`

    `true` when it is safe to send the call two times: it has its own idempotency key, or it only
    reads. Default `false`. After an Orchestrator restart, jr2 cannot always tell if the call went
    out. Then it sends an idempotent call again, and gives any other call the output `unknown`.

**Returns**: An Effect, for `actors`.

**Example**

```ts
import { z } from "zod";
import { bearer, http, jr2Setup, secret } from "@jr2/orchestrator";

const comment = http({
  method: "POST",
  url: "https://jira.example.com/rest/api/2/issue",
  auth: bearer(secret("jira")),
  input: z.object({ key: z.string(), body: z.string() }),
  request: ({ key, body }) => ({ path: [key, "comment"], json: { body } }),
});

export const machine = jr2Setup({
  types: {} as { context: { key: string }; input: { key: string } },
  actors: { comment },
}).createMachine({
  id: "comment",
  context: ({ input }) => input,
  initial: "commenting",
  states: {
    commenting: {
      invoke: {
        src: "comment",
        input: ({ context }) => ({ key: context.key, body: "The build passed." }),
        onDone: "done",
      },
    },
    done: { type: "final" },
  },
});
```

**See also**

- [Effects](../../concepts/effects.md)
- [Run a command or call a URL with an Effect](../../guides/run-a-command-or-call-a-url.md)
- [Secrets and trust](../../concepts/secrets-and-trust.md)
- [ADR-0068: An Effector holds an Effect's secrets and signs its wire](../decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)
- [ADR-0067: An Effect is an actor slot, and runs at most once unless marked idempotent](../decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)
