http
Function, in @jr2/orchestrator/src/http-effect.ts. It is an export of @jr2/orchestrator, on the Authoring surface page, in Effects.
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 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:HttpMethodThe HTTP method:
GET,HEAD,POST,PUT,PATCH,DELETEorOPTIONS. It refuses another:http({ method: "<method>" }): the method is one of GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS.url:string | Text | typeof openThe url:
http://orhttps://, 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 withcustomize(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`
- a scheme or host that is not literal:
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
textwith 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
authorizationheader besideauth:http({ headers }): "authorization" is set by `auth` — write one of them
- a name that is not lowercase, or that frames or routes the request (
auth(optional):{ readonly bearer: Secret; } | { readonly basic: { user: Text; password: Text; }; } | TextThe
Authorizationheader:bearer(secret("x")),basic(user, password), or atextthat is the whole value.input(optional):SThe zod schema of the input of the Effect: what the
inputof theinvoke, orfromPick, 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 (itsonErrorroute):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) => HttpRequestPartsA function that makes the part of the request that changes for each run, from the input:
pathsegments, and ajsonortextbody. It cannot change the host, a header or an Effect secret. Do not write it besidebody.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 writerequestbeside it: thesegmentsoffileare the path. It refuses another body:http({ body }): a body is file(path, { slot?, segments?, contentType? }) — json and text come from `request`.timeout(optional):DurationHow 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 isunknown. It refuses a value that is not a positive duration:http: timeout "<timeout>" is not a positive duration (like "30s", "2m").maxBody(optional):numberThe largest number of bytes of a text response body that the output keeps. Default 65536. When jr2 cuts the body,
truncatedistrue. It refuses a value that is not a positive whole number:http({ maxBody }): a positive number of bytes.idempotent(optional):booleantruewhen it is safe to send the call two times: it has its own idempotency key, or it only reads. Defaultfalse. 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 outputunknown.
Returns: An Effect, for actors.
Example
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