Lesson 38 — HTTP calls by function identity
Goal: name a published handler by UUID, resolve its public URL, make an actual HTTP GET, and receive a changed response after editing the graph.
You need: the authenticated finite HTTP host from lesson 35. Ordinary cloud users can take this lesson; it creates no persistent service or listener port.
Try it
- Extend
text-ok-responseastutorial-contract-answerand bind its:bodytohello. Choose ⋯ → HTTP → Publish for 30 minutes. - Extend
service-endpointastutorial-http-address. Bind its:serviceslot with Bind fn-ref totutorial-contract-answer. This stores the handler UUID without executing it. - Run the address function and confirm the database effect if requested.
Read its result:
host,port, andurl. Keep the whole URL, including any publication path. On a deployment behind TLS this is an HTTPS URL. - After Run finishes, copy that URL into another browser tab. The real HTTP
response must show
hello. Return to the editor. - Edit the handler's bound literal to
updatedand save. Leave its publication and the address function's fn-ref unchanged. - Reload the same public tab. Its response must now be
updated. - Stop the publication through ⋯ → HTTP. Reload its URL to verify it is unavailable, then finish the lesson and remove its created items.
These requests run sequentially, so the exercise also works with one execution slot. A busy executor may return 503: finish another run and retry. The browser request is real HTTP, and the published handler still obeys its execution quota, current permissions, effect restrictions, and deadline.
Calling from a graph
service-get composes the same identity resolution with an ordinary outbound
HTTP GET. Bind :service to the handler's fn-ref and :path to /hello. Its
result contains the HTTP status, headers and body. Network permissions, outbound
address checks, rate limits and timeouts still apply.
A graph calling a handler on its own executor needs capacity for both calls: its Run holds an execution slot and worker while waiting for the HTTP response. With one organization slot, the handler returns 503. A saturated shared worker pool can also cause a timeout. Use sufficient capacity or a separately hosted endpoint for such nested calls; publishing does not increase these limits.
Core resolves a base URL containing its reserved publication path; cloud resolves an isolated apps origin. Do not strip that base path when building a request. Never put a private bearer token in the public URL. The handler receives sanitized request fields and returns bounded plain text or JSON.
The same identity seam for persistent services
On a self-hosted or dedicated executor, service-endpoint first prefers an
active listener instance on the caller's branch. The reconciler maintains its
host, port and heartbeat. A temporary publication is a separate fallback, and
an ordinary configured cloud app may also resolve through the existing addon.
The function UUID still names the target without invoking it. No active endpoint
means service/not-running; it does not guess another branch.
The following sections describe shared contracts and the existing persistent service model. Temporary publications do not create service-instance rows or promise persistent-service tracing.
The contract lives in the graph
For a larger application, define paths and response shapes once, then reference them from both the producer and consumer:
;; svc-orders.api — the contract: paths + shapes, owned by the producer's team
{:name :orders-path :parent :const :args {:value "/orders"}}
{:name :orders-shape :type {:orders [:list :int]}}
;; the producer's route, built from the contract
{:name :orders-route :parent :get-route
:args {:path :orders-path :handler-fn :orders-ok}}
;; the consumer, built from the same contract
{:name :fetch-orders :parent :service-get-json
:args {:service :orders-service :path :orders-path}}
Change the path constant's value and both the route and consumer use the new
path. Narrow :orders-shape and the type-checker reports which graph no
longer matches. The contract is an ordinary type function (lesson 08),
so no separate interface-definition file is needed. With two teams in one
org, the contract namespace belongs to the producer's team and the
consumer's team holds read on it (lesson 28 — a role can be the
grant's subject, so the team is one row).
On a multi-pod fleet, and on the cloud
- Fleet (docs/SCALING.md): the recorded host is the pod's
executor-id— the same pod-FQDN the fleet's forward-hop dials — so a:singletonproducer on pod 2 is reachable from a consumer on pod 1. A:per-podlistener records whichever pod started last; any of them serves. - Cloud: a tenant has no ports. A fn published as an app (lesson
30) resolves to its public origin,
https://<label>.graphden.app, and the call is an ordinary outbound request — egress-guarded and rate-capped like any other. The graph is the same; only the answer to where differs. - Mutual calls are legal:
svc-amay namesvc-bandsvc-bnamesvc-a. The:fn-refedge is an identity, not a dependency, so the cycle rule (lesson 13) does not fire.
Following a call across services
For persistent HTTP services, Trace (lesson 18) can follow a call beyond
:http-get. Run :fetch-orders from the Run pane
and open the run's result: under it, a Downstream calls list names
:orders-ring, the producer's handler, with its status. Open the
producer's Runs tab: the request :fetch-orders made is a run of its
own there, and its details show the same trace id as the caller.
The mechanics: a persisted run knows its id; :service-get sends it
along as X-Graphden-Trace; :http-server sees the header and
records the request it handles as an execution linked to the caller
(a request without the header is not recorded — normal traffic pays
nothing). Every hop shares the top-level run's trace id, so a chain
A → B → C is one tree you can walk from A
(docs/EXECUTION.md § Tracing across services).
What we glossed over
- Why
:fn-refis its own type and not the HOF:fnslot — the HOF slot hands the impl something to call, and for a fn that returns a callable (every listener does) that means evaluating it. See docs/TYPES.md. - Liveness: a copy that dies in place (the listener stops, a daemon
thread ends) is noticed on the next tick and restarted per the row's
restart-policy; a pod that crashes leaves a row whose heartbeat goes stale, so consumers stop picking it within 45 seconds (docs/SERVICES.md § Liveness). - Asynchronous work between services is the next lesson (39 — Queues).
Next
Lesson 39 — Queues: asynchronous work between services.