Send and receive messages over HTTP
The HTTP source takes the body of each POST (or PUT, when you allow it) as one message. The HTTP
destination POSTs each output to a URL. Both run over HTTPS unless the block says plaintext: true,
and both work with any format.
Take messages over HTTPS
format: fhir
source:
http:
port: 8443
path: /results
tls:
cert: ../../certs/server.pem
key: ../../certs/server.key
auth:
bearer: { env: RESULTS_TOKEN }
maxBytes: 5MB
destinations:
- file: { dir: ../../data/results-in/out }
hostis127.0.0.1by default; set it to take requests from other machines.pathis/by default. Any other path answers 404. To listen on several, list them inpathsinstead, such aspaths: [/adt, /results]; a channel sets one or the other.methodsis[POST]by default.[POST, PUT]takes both, and[PUT]takes PUT alone.responsesays how a stored message is answered: by default 202 with its id as JSON. See Answer with an HL7 ACK and Answer with a fixed response.authis a bearer token by default, read at start from the environment variablebearer.envnames, or fromPATHWAYS_HTTP_TOKENwhenauthis not set. A token is at least 16 characters of letters, digits and-._~+/, and never goes in the YAML. Withtls.caset and noauth, mutual TLS authenticates instead;auth: noneturns authentication off.maxBytesis 10 MB by default.
A sender POSTs the message as the request body:
curl --cacert certs/ca.pem \
-H "Authorization: Bearer $RESULTS_TOKEN" \
--data-binary @patient.json \
https://pathways.example.com:8443/results
The source answers once it knows what became of the message, and never echoes the body:
| Status | When |
|---|---|
| 202 | stored; the body is {"id": <message id>} |
| 400 | the body is empty, or the path breaks the path rule below |
| 401 | the bearer token is missing or wrong |
| 404 | a path the source does not listen on |
| 405 | a method methods does not name; Allow lists the ones it does |
| 413 | the body is larger than maxBytes |
| 415 | the body is compressed (Content-Encoding other than identity) |
| 503 | the message could not be stored; the sender should send it again |
How a request's path is matched
Each request's path is read by one rule, and the paths you configure pass the same rule when the configuration loads:
- The path is everything before the first
?. The query string never changes which path matched. - A
\, or an encoded/or\(%2F,%5C, in either case), answers 400: decoded, it would move a boundary between segments. - Every other percent-encoded byte is decoded as UTF-8, so
/lab%20resultsis/lab results. An encoding that does not decode, or a control character, answers 400. - One trailing
/is ignored, except on/itself:/adt/is/adt. - An empty segment (
//) or a dot segment (.or.., written or encoded) answers 400. Nothing is resolved, so/adt/../adminnever reaches/admin.
What remains is compared exactly, letter case included, with each configured path. A configured path
holds no %, ? or #, and two that read the same, such as /adt and /adt/, are refused.
Answer with an HL7 ACK
HL7 over HTTP is a draft transport for HL7 v2 messages in HTTP requests. It is published as HL7 over HTTP on NIST's v2+ pages and as the transport chapter of HL7 Europe's v2+ rendering. What Pathways takes from it:
- A message is the body of a POST, with the media type
x-application/hl7-v2+er7for the pipe and hat encoding. - The receiver answers 200 with the HL7 ACK as the body, in the same media type, whether the ACK is
AA,AEorAR: the ACK, not the HTTP status, says what became of the message. - An HTTP error status means the request itself failed, and its body is not an HL7 message.
With response: ack, on a channel whose format is hl7v2, the source answers that way:
source:
http:
port: 8443
path: /hl7
response: ack
tls:
cert: ../../certs/server.pem
key: ../../certs/server.key
destinations:
- file: { dir: ../../data/adt-in/out }
- The message is stored first, and the ACK is sent only once it is:
AA, built the way the MLLP source builds it, with MSA-2 echoing the message's MSH-10. - A message with no usable MSH header (none that parses, no MSH-10, or more than one message in the
body) is stored as failed and answered
AE, at 200. The engine logshttp-unreadablewith the message id and the problem's code. - The request's
Content-Typemust bex-application/hl7-v2+er7orapplication/hl7-v2, in any letter case, with any parameters. The ACK goes back in the same one, withcharset=utf-8, orcharset=iso-8859-1when the message's MSH-18 names an ISO 8859 character set, as the ACK is then written in it. Any other media type, or none, answers 415. - POST is the only method, whatever
methodssays, and another answers 405 withAllow: POST. - When the message cannot be stored, the answer is 503 with no ACK, so the sender sends it again.
- Every answer that is not 2xx (400, 401, 404, 405, 413, 415, 500, 503) is plain text, never JSON and
never HL7. Each answer carries a
Dateheader.
A sender POSTs the message as the body:
curl --cacert certs/ca.pem \
-H "Authorization: Bearer $PATHWAYS_HTTP_TOKEN" \
-H "Content-Type: x-application/hl7-v2+er7; charset=utf-8" \
--data-binary @a01.hl7 \
https://pathways.example.com:8443/hl7
Answer with a fixed response
Some senders want a particular status or header back. response can give the same answer to every
stored message:
format: json
source:
http:
port: 8443
paths: [/orders, /orders/v2]
methods: [POST, PUT]
response:
status: 200
contentType: text/plain
headers:
X-Receiver: pathways
body: OK
tls:
cert: ../../certs/server.pem
key: ../../certs/server.key
destinations:
- file: { dir: ../../data/orders-in/out }
statusis a 2xx status, 200 to 299, checked when the configuration loads.contentTypeis theContent-Typesent, a media type such astext/plain. None is sent when it is not set.headersare sent with every answer, by the HTTP destination's rules: a valid name and value, and a header whose name suggests a credential read with{ env: NAME }.Content-Type,Content-Length,Date,Connection,Keep-AliveandTransfer-Encodingare set by Pathways and cannot be set here. Nothing is copied from the request.bodyis the text sent, at most 4,096 characters, empty by default, and none with a 204 or 205. It is what you wrote, never the message.
Refusals and a store failure are answered as in the table above.
POST each output to a URL
source:
file: { dir: ../../data/adt-to-fhir/in }
transform: ./transform.ts
destinations:
- http:
url: https://fhir.example.com/Bundle
headers:
Authorization: { env: FHIR_SERVER_AUTH }
X-Source: pathways
timeout: 30s
urlishttps://. Anhttp://URL needsplaintext: true, and credentials never go in the URL.headersare sent with every request. A header whose name holdsauth,cookie,token,secret,password,key,signatureorcredential, in any case, carries a credential, so it must be read from the environment with{ env: NAME }.Content-Typeis set from the channel's format (application/hl7-v2,application/fhir+json,application/edi-x12,application/xml,application/jsonortext/plain) unless you set it.tls.catrusts your own CA in place of Node.js's built-in roots;tls.certandtls.keypresent a client certificate. The server's certificate is always verified.timeout(30 seconds by default) is how long one attempt may take, from connecting to the answer.resendHeader, when set, names a header sent only on a possible duplicate: a resend after an attempt was cut off by a stop, so the server may have the message already. Its value is the number of attempts before, such as1. The body is the same either way. See Delivery and errors.
The answer decides what happens:
| The server answers | The delivery |
|---|---|
| 2xx | delivered |
| 408, 425, 429, 5xx, no answer in time, a network error | retried, with later outputs waiting behind it |
| 3xx | held: retried every 5 minutes, never followed, with its own event |
| 101, any other 4xx | failed for good; the destination moves on |
A retry waits 1 second, then 2, 4, 8 and so on, up to 5 minutes. When a 429 or a 503 carries a
Retry-After header, the retry waits as long as it asks if that is longer, still never more than 5
minutes. Retry-After is read as a whole number of seconds (Retry-After: 120) or as an HTTP date
(Retry-After: Wed, 11 Nov 2026 09:30:00 GMT), and a date already past counts as no wait. A value
that is negative, has a fraction, is empty, has more than nine digits or is not a date that exists,
or a header sent twice, is ignored, and the retry waits its usual time. On any other status the
header is ignored. The time of the next attempt is stored, so a restart keeps the wait.
Redirects are not followed, since following one could send a message to a host nobody configured. A
3xx answer holds the delivery: no connection is made to its Location, the delivery is tried again
every 5 minutes, and the engine logs delivery-redirected and sends hooks a delivery-redirected
event, each naming the status and the host the Location names, never its path or query. Fix the
destination's url and the queue moves on. The response body is read and dropped, never logged.
Related
- Authenticate endpoints and use TLS.
- Check status and health: the health endpoint runs on the same server code.
- Configuration reference: HTTP.