HTTP Source agent
Source HTTP Source (Courier inbound)
The HTTP Source agent is the same courier-agent agent as the HTTP Target, assigned to a pipeline as its source. It opens its own HTTP listener and serves the Courier inbound write endpoints that run through a pipeline, called pipeline endpoints: every authenticated POST, PUT or DELETE on one of them is turned into one row, written to a durable queue on the local disk, forced to the disk and answered 202 Accepted with a sequence number. The pipeline then delivers the queued rows to its target, whatever it is: a relational database, a NoSQL store, Kafka, an object store or another HTTP Target.
Use it when an application, a SaaS webhook or a device can push records over HTTP and you want them to land in a database or a stream with the guarantees of a pipeline (checkpoint, retry, ordering, column mapping) instead of writing code that talks to the target directly. The client is never held on the target: the answer comes as soon as the row is on disk, and a stopped or failing target only makes the queue grow until it comes back.
|
Courier inbound endpoints that read ( |
How a request becomes a row
HTTP client ── POST /api-inbound/<basePath>/<path> ──▶ HTTP Source listener (port 8090 by default, inside Core Hub)
◀── 202 {"sequence": n} / 4xx / 503 ───────│ limits → match → token → bind → queue on disk, forced
▼
Core Hub polls the queue ──▶ target writes the row ──▶ row confirmed and released
-
Limits. A body larger than
maxBodyKbis refused with 413, a request beyondmaxInFlightwith 503, and a request not received within 60 seconds with 408: the server never queues work it cannot bound. -
Match. The URL selects the collection by its base path, one URL segment, and the endpoint by its verb and path template; segments are compared left to right and a literal segment wins over a
{placeholder}at the first position where two templates differ. -
Authentication. The Personal API Token in the
Authorization: Bearerheader is checked against the endpoint’s pipeline and HTTP Source agent, the role of its owner, and the collection’s allowlist and blocklist. Verdicts are cached on the agent for at most a minute, so Core Hub is asked only on a cache miss. -
Binding. Path placeholders, query parameters, headers and body fields are read into the endpoint’s bindings, typed and checked against the columns they are announced with. Every binding becomes one column of the row; the operation is
Insert,UpdateorDelete. -
Queue. The row receives the next sequence number and is appended to the queue of every entity built on the endpoint, then forced to the disk. Only then is the request answered
202. -
Delivery. Core Hub polls the queue like any other source, the target writes the rows in order, and each row is released only once the target confirmed it.
Steps 1 to 5 never call the target, never wait for a pipeline and, apart from a token cache miss, never call Core Hub.
Delivery guarantees
-
Durable acceptance. A
202means the row is in the queue file and has been forced to the disk (an fsync) before the answer: it survives a crash or a restart of the Core Hub process and a crash or a power loss of the host. Requests that arrive together share one fsync, so the throughput of a busy endpoint is kept. When the force fails the client gets503, and the row may be queued anyway. A request that got no202(a timeout, a connection reset, a503, any error) may therefore have been queued: the client retries it, and the target may receive the row twice. -
At-least-once. A row is delivered until the target confirms it, across restarts, target outages and entity stops; nothing answered
202is dropped by a retention policy, because the queue has none. A row can reach the target more than once after a crash between the write and its confirmation, and a client that retries a request it already got202for queues it twice. -
Ordered per endpoint. Rows of one endpoint are delivered in the order their
202sequences were assigned. Rows of different endpoints are not ordered with respect to each other. -
Unique sequences. A sequence floor is saved to disk ahead of every confirmation, so a sequence number is never handed out twice, across restarts too. After a restart the numbering can skip some numbers (up to about a thousand): a sequence is an identity, not a count.
Prerequisites
To have Gluesync ingest HTTP requests through a pipeline you will need to have:
-
Core Hub 2.3.2.1 or above, which ships the HTTP agent and the Courier module;
-
A Core Hub user who can manage connections (a Manager or a Super Admin) to define the collections and endpoints in Courier;
-
A pipeline whose source is the HTTP agent (the HTTP Source) and whose target is any Gluesync target agent;
-
The HTTP Source listening port (
8090by default) reachable by the clients on the host that runs Core Hub; open it in the firewall, or publish it from the container, for example-p 8090:8090; -
A TLS keystore for the listener, or a TLS terminator in front of it: without one the listener refuses to serve plain HTTP on any address but loopback, unless
inboundAllowPlainHttpis set, see Security; -
A Personal API Token for each client, owned by a Manager or a Super Admin, carrying the HTTPS agent permission scope (
gluesync.https_agent) and either no agent scope or the one of the pipeline and the HTTP Source agent. Core Hub refuses to save a collection that no active token can call, so create the first token before the collection, see Personal API Token; -
Enough local disk for the queue: up to
maxTotalBacklogMb(8 GB by default) in thehttp_cachedirectory under the agent’s data directory, andminFreeDiskMb(1 GB by default, or 5% of the volume if that is more) kept free.
Setup via Web UI
Pipeline: add the HTTP Source
Create a pipeline and choose the HTTP agent as its source; the agent runs inside Core Hub and needs no host, port or credentials to reach it. Its listener starts as soon as the agent is configured, before any entity exists, and only if its TLS settings are valid: set a keystore, bind to 127.0.0.1 behind a TLS terminator, or set inboundAllowPlainHttp.
The Custom host credentials of the source are all optional:
| Property | Default | Meaning |
|---|---|---|
|
|
TCP port of the HTTP Source listener. Every HTTP Source instance in the same Core Hub needs its own port; a port already in use stops the agent with an error naming the address and the port. |
|
|
Interface the listener binds. Use |
|
empty (all) |
Name, id or base path of the only inbound collection this instance serves. Empty: every collection holding pipeline endpoints of this agent. |
|
|
Largest request body. A larger |
|
|
Requests being authenticated, bound and queued at the same time. One more gets 503 with |
|
|
Bytes queued for one endpoint and not yet confirmed by the target. Beyond it new requests on that endpoint get 503 with |
|
|
Space the agent’s queue files have in use, rows already confirmed but not yet recycled included. Beyond it every new request gets 503 with |
|
|
Threads serving requests (at most 512). They are separate from Core Hub’s own threads. |
|
|
Connections the operating system holds before the server accepts them (at most 65535). Raise it for bursts of new connections. |
|
|
Threads accepting new connections (at most 8). |
|
empty |
PKCS12 or JKS keystore file. When set, the listener serves HTTPS only; otherwise plain HTTP, which is accepted on loopback only, or elsewhere with |
|
empty |
Password of the keystore and of its private key. Required when a keystore path is set. |
|
empty (first private key) |
Alias of the key to serve. |
|
|
Personal API Tokens travel in clear over plain HTTP, so without a keystore the listener does not start on an address other than loopback: the agent stops with an error that says so. |
|
|
Free space the queue’s directory must keep, or 5% of the volume if that is more. Below it new requests get 503 with |
Values that are not positive integers fall back to their default.
Courier: define the pipeline endpoints
In create a collection, then its endpoints.
-
Collection (New collection)
-
Collection name: the source schema the pipeline will see.
-
Base path: the first URL segment of every endpoint of the collection, served at
/api-inbound/<base path>/…. It is one segment, made of letters, digits,.,_,~and-, without/, and unique across collections. -
Description (optional).
-
Access token: Core Hub saves a collection only while at least one active Personal API Token with the HTTPS agent permission can call it, whoever owns the token. The form says how many of your tokens can, and offers Also create a new token for this collection: it creates a token with the HTTPS agent permission only, for all agents, with the name and expiry you choose, shows its value once after the collection is saved and lets it call the collection. When none of your tokens can, the box is ticked for you.
-
The Allowlist and the blocklist are not part of this form: you manage them for each token in the Access tokens section of the collection, see Personal API Token and Authentication.
-
-
Endpoint
-
Endpoint type:
Pipeline rows · the HTTP Source queues each request. (Database table · CoreHub runs the SQLis the existing SQL endpoint.) -
Endpoint name: the source table the pipeline will see.
-
Operation and HTTP method:
CREATEwithPOST,UPDATEwithPUT,DELETEwithDELETE.PATCHis not available, because a row carries every binding and replaces the target row. -
HTTP Source (pipeline · agent): the pipeline, and its HTTP Source agent, that serves the endpoint and queues its rows.
-
Path: relative to
/api-inbound/<basePath>, starting with/; every{name}placeholder is a whole segment, e.g./orders/{id}. -
Bindings: the values read from the request, one source column each:
-
Name: the column name, and the placeholder, query parameter, header or body field it is read from;
-
Location:
PATH(a path placeholder),QUERY(a query parameter),HEADER(a request header, matched ignoring case) orBODY(a body field); -
Type:
STRING,NUMBERorBOOLEAN, which decides the column type, see Supported data types; -
Required: (defaults to
true) a request without the value and without a default is refused with 400; -
Default value: used when the request does not carry the value.
-
-
Key bindings: the bindings that identify the row on the target. Empty: the required
PATHbindings, or the request sequence_seqwhen there are none. -
Operation binding (optional): a
STRINGbinding whose value,Insert,UpdateorDelete(ignoring case), overrides the operation, so one endpoint can do all three. When the request leaves it out, the endpoint’s operation applies. -
Body type:
JSON(default) orFORM_URLENCODED, for theBODYbindings ofPOSTandPUTendpoints.
-
The endpoint shows the URL it is served at, on the HTTP Source port. The Preview the row panel binds sample path, query, header and body values exactly as the agent would and shows the row it would queue, with its operation and key, or the reasons it would answer 400. Nothing is written.
Every change made in Courier is pushed to the running HTTP Sources at once; no restart is needed. Changing or deleting an endpoint that a pipeline entity reads is refused with 409 unless confirmed (Save anyway, Delete anyway, or ?force=true on the REST API), because it breaks the entity’s mapping.
How the request is bound
-
PATHvalues come from the placeholders,QUERYvalues from the query string (the first value of a repeated parameter),HEADERvalues from the headers, matched ignoring case. -
A
JSONbody must be an object. ABODYbinding namedcustomer.address.cityreads that key if it exists as is, otherwise walks the nested objectscustomer→address→city. An object or an array bound to a binding is stored as its raw JSON text. -
A
FORM_URLENCODEDbody is read as URL-decoded pairs; the first value of a repeated name wins. -
An optional binding the request leaves out takes its default, or
null. An explicit JSONnullstaysnulleven when the binding has a default. -
Every row carries every binding, so an
Updatereplaces the target row as aPUTdoes: a binding the client did not send is written as its default ornull. -
An endpoint without key bindings and without required
PATHbindings gets a synthetic_seqkey holding the request sequence: every request is a new row. This is the shape for append-only targets such as Kafka, object stores or audit tables. -
A value shaped like a masked one is refused. Courier
READandLISTendpoints answer the columns Query Studio classifies as personal data masked (a@.com,••••67) while masking is on. A client that sends back an object it read would store the mask in place of the value, so while masking is on a request carrying a value with the shape of a mask (••••, alone or followed by two or four characters, orx@.tld), in any binding, is refused with 400 and nothing is queued. Send the value itself, or leave the field out.
Validation rules
Courier refuses to save a pipeline endpoint that breaks one of these rules; the same rules apply to the REST API.
-
At least one binding; binding names start with a letter or
_, continue with letters, digits,_,.or-, and are unique ignoring case._seqis reserved. -
Every
{placeholder}of the path has a requiredPATHbinding of the same name, and everyPATHbinding has its placeholder. -
Key bindings are declared and required.
-
UPDATEandDELETEendpoints need a key (a key binding or a requiredPATHbinding), and so does any endpoint with an operation binding: with the_seqkey an update or a delete would find no row and change nothing. -
The operation binding is declared and of type
STRING. -
PATCHis refused; the verb must fit the operation (POSTforCREATE,PUTforUPDATE,DELETEforDELETE). -
BODYbindings only onPOSTandPUT, with a body type other thanNONE. -
A
HEADERbinding has a valid header name and is notAuthorization,Proxy-AuthorizationorCookie: those carry the caller’s credentials, which would end up in the target. -
A default value fits its column (see Supported data types).
-
Two endpoints of the same collection cannot share the verb and the path shape.
-
On the collection: the base path is one segment and unique, and a token cannot be on the allowlist and on the blocklist at once.
Personal API Token
The Access tokens section of the collection lists your active Personal API Tokens that can call it: those that carry the HTTPS agent permission (or no permission restriction) and reach at least one of its agents. Each row shows the agents it covers (All agents or Covers n of m) and where it stands with the collection: No allowlist, Allowed, Not on the allowlist or Blocked.
-
New token creates a token for the inbound endpoints. It carries only the HTTPS agent permission, and its agent scopes are prefilled with the pipeline and HTTP Source agent the collection’s endpoints use, or All agents. A box adds it to the allowlist at once, which turns the allowlist on. The token (
gsp_…) is shown once: copy it and hand it to the client; clients send it asAuthorization: Bearer <token>. -
The Allowlist toggle (
Off · any token within its scopesorOn · only the tokens on the allowlist) turns the allowlist on or off. Before it changes who can call the collection, the dialog says how many tokens gain or lose access. -
The menu of each token has Add to allowlist / Remove from allowlist, Block / Unblock and Revoke. A blocked token can no longer call the collection, whether the allowlist is on or off; its other collections are not affected.
-
Core Hub refuses a change that would leave no active token able to call the collection, for instance blocking the last one. The panel lists only your own tokens, so it can only warn you; Core Hub’s answer decides.
-
Revoke is for good: clients using the token get 401 from every endpoint, and it leaves the allowlist and blocklist of this collection too.
The token permission is gluesync.https_agent, shown as HTTPS agent.
Entity: map the endpoint onto the target
Create the entity as for any other source: the collection is the source schema, the endpoint is the source table and its bindings are the columns, plus _seq when the endpoint has no key. Only collections holding at least one pipeline endpoint are listed. Map the columns onto the target table, or let Gluesync create it from the announced types.
|
The entity of an endpoint that only deletes must have only the key columns as target columns. Such an endpoint usually binds nothing but the key, while the target table has more columns, and the relational targets refuse a row that does not cover every target column of the entity: the delete would stay at the head of the queue and block the entity until the mapping is narrowed to the key. An endpoint that writes all three operations through an operation binding carries every column and has no such restriction. |
-
Snapshot. The HTTP Source has no data at rest: the snapshot is empty and completes at once. Rows queued for the entity before it started flow right after it.
-
Re-sync keeps the queue: rows answered
202and not yet delivered are delivered after the (empty) snapshot. -
Deleting the entity drops its queue, undelivered rows included.
-
Stopped or failing entity. Requests keep being accepted and queued, up to the backlog limits; the rows flow when the entity starts again or the target comes back.
-
No entity. A request on an endpoint that no entity of the pipeline reads is refused with 404: there is no queue to write it to.
-
One endpoint read by several entities of the same pipeline is queued once per entity, with the same sequence.
Setup via Rest APIs
Here following an example of calling the Core Hub’s Rest API via curl to connect the agent and to define a token, a collection and two pipeline endpoints. Ids of collections and endpoints are chosen by the caller; a POST with an id already stored is refused with 409, as a stored definition is changed with PUT. The token comes before the collection: Core Hub refuses to save a collection that no active token can call.
Connect the agent
curl --location --request PUT 'http://core-hub-ip-address/pipelines/{pipelineId}/agents/{agentId}/config/credentials' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
"hostCredentials": {
"connectionName": "HTTP Source"
},
"customHostCredentials": {
"inboundPort": 8090,
"inboundCollection": "shop",
"inboundAllowPlainHttp": true
}
}'
inboundAllowPlainHttp is there so that this example, which calls the listener over plain HTTP, starts: without a keystore the listener refuses any address but loopback unless it is true. Use it only when TLS is terminated before the agent, or for a trial on a trusted network; otherwise set inboundTlsKeystorePath and inboundTlsKeystorePassword.
Create a token for the collection
curl --location --request POST 'http://core-hub-ip-address/api-tokens' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
"name": "shop webhook",
"expiresInDays": 365,
"permissionScopes": ["gluesync.https_agent"],
"scopes": [ { "pipelineId": "{pipelineId}", "agentId": "{httpSourceAgentId}" } ]
}'
The request needs the session of a Control Plane user (a token cannot create tokens), who becomes the owner of the new token and must be a Manager or a Super Admin for the token to write. The answer carries the token in clear, once, and its id. To restrict a collection to some tokens, set allowlistEnabled to true and put their ids in allowedTokenIds; to lock a token out, put its id in blockedTokenIds. POST /api-tokens/{id}/revoke revokes it.
Create the collection
curl --location --request POST 'http://core-hub-ip-address/api-agent/inbound/collections' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
"id": "3d0c2f5e-8a71-4b7e-9c55-0e6f1a2b3c4d",
"name": "shop",
"basePath": "shop",
"allowlistEnabled": false,
"allowedTokenIds": [],
"blockedTokenIds": []
}'
basePath is one segment. allowlistEnabled, allowedTokenIds and blockedTokenIds are explained in Authentication; a token cannot be in both lists. A save that would leave no active token with the gluesync.https_agent permission able to call the collection, once the lists are applied, is refused with 400.
Create the pipeline endpoints
An endpoint that inserts, updates or deletes an order, chosen by the op field of the body:
curl --location --request POST 'http://core-hub-ip-address/api-agent/inbound/endpoints' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
"id": "8b1e4a6c-2d3f-4e5a-9b8c-7d6e5f4a3b2c",
"name": "orders",
"collectionId": "3d0c2f5e-8a71-4b7e-9c55-0e6f1a2b3c4d",
"path": "/orders/{id}",
"httpVerb": "POST",
"operation": "CREATE",
"pipelineId": "{pipelineId}",
"agentId": "{httpSourceAgentId}",
"bodyType": "JSON",
"operationBinding": "op",
"bindings": [
{ "name": "id", "type": "NUMBER", "location": "PATH", "required": true },
{ "name": "op", "type": "STRING", "location": "BODY", "required": false },
{ "name": "status", "type": "STRING", "location": "BODY", "required": true },
{ "name": "amount", "type": "NUMBER", "location": "BODY", "required": true },
{ "name": "paid", "type": "BOOLEAN", "location": "BODY", "required": false, "defaultValue": "false" },
{ "name": "customer.address.city", "type": "STRING", "location": "BODY", "required": false },
{ "name": "X-Tenant", "type": "STRING", "location": "HEADER", "required": false, "defaultValue": "acme" }
]
}'
An append-only endpoint: no key binding and no PATH binding, so every request is a new row keyed by _seq:
curl --location --request POST 'http://core-hub-ip-address/api-agent/inbound/endpoints' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
"id": "c4d5e6f7-0a1b-4c2d-8e3f-a4b5c6d7e8f9",
"name": "events",
"collectionId": "3d0c2f5e-8a71-4b7e-9c55-0e6f1a2b3c4d",
"path": "/events",
"httpVerb": "POST",
"operation": "CREATE",
"pipelineId": "{pipelineId}",
"agentId": "{httpSourceAgentId}",
"bodyType": "JSON",
"bindings": [
{ "name": "type", "type": "STRING", "location": "BODY", "required": true },
{ "name": "device", "type": "STRING", "location": "QUERY", "required": true },
{ "name": "payload", "type": "STRING", "location": "BODY", "required": false }
]
}'
GET /api-agent/inbound/collections, GET /api-agent/inbound/endpoints?collectionId=…, PUT /api-agent/inbound/collections/{id}, PUT /api-agent/inbound/endpoints/{id} and the matching DELETE routes complete the API. A PUT or DELETE that would break an entity reading the endpoint answers 409 unless ?force=true is added.
Calling an endpoint
Pipeline endpoints are served only by the HTTP Source, at:
http(s)://<core-hub-host>:<inboundPort>/api-inbound/<basePath>/<path>
Core Hub’s own /api-inbound route answers 409 for a pipeline endpoint, after authenticating the caller.
curl -i -X POST 'http://core-hub-host:8090/api-inbound/shop/orders/42' \
-H 'Authorization: Bearer gsp_…' \
-H 'Content-Type: application/json' \
-H 'X-Tenant: north' \
-d '{"status":"new","amount":99.90,"customer":{"address":{"city":"Turin"}}}'
HTTP/1.1 202 Accepted
Content-Type: application/json
{"sequence":1337,"collection":"shop","endpoint":"orders","operation":"Insert"}
The row queued is id=42, op=null, status='new', amount=99.90, paid=false, customer.address.city='Turin', X-Tenant='north', with operation Insert and key id.
curl -i -X POST 'http://core-hub-host:8090/api-inbound/shop/orders/42' \
-H 'Authorization: Bearer gsp_…' \
-H 'Content-Type: application/json' \
-d '{"op":"Delete","status":"cancelled","amount":0}'
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"error":"Binding 'amount' must be a number","reasons":["Binding 'amount' must be a number"]}
Every refusal has the same body: error with a message and, for a 400, reasons with every reason the request could not become a row. The sequence of a 202 is the position of the row in the queue; keep it in the client’s log to trace a row to the target.
Status codes
The server applies its checks in this order and answers with the first that fails:
| # | Status | When | Retry-After |
|---|---|---|---|
1 |
405 |
The HTTP method is not one Courier knows ( |
|
2 |
413 |
The |
|
3 |
503 |
|
|
4 |
404 |
No collection has the base path, or no endpoint of the collection matches the verb and the path, and the caller holds a token the HTTP Source accepts. A wrong verb on an existing path is a 404 too. A request that matches nothing and carries no valid token gets the 401 below instead, so that nobody learns which routes exist before authenticating. |
|
5 |
401 |
No |
|
5 |
403 |
The token lacks the HTTPS agent permission scope ( |
|
5 |
503 |
The token is not in the agent’s cache and Core Hub cannot be asked now (unreachable, busy or slower than 3 seconds). The token may well be valid: retry. |
|
6 |
503 |
The free space of the queue’s disk is below |
|
7 |
413 |
The body read is larger than |
|
8 |
400 |
The request cannot become a row: a required binding missing, a value of the wrong type or too long, an unreadable body, a JSON body that is not an object, nests deeper than 64 levels or repeats a key, an operation binding with a value other than |
|
9 |
404 |
No entity of the pipeline reads the endpoint, so there is no queue to write the row to. |
|
10 |
503 |
The queue is not open yet (agent starting or stopping). |
|
11 |
400 |
While Core Hub masks personal data, a value of the request has the shape of a masked value (see How the request is bound). A |
|
12 |
503 |
The row could not be written or forced to the disk (for example, disk full). Nothing is acknowledged, but a row that was written before the force failed may stay queued. |
|
13 |
202 |
The row is queued and on the disk: |
|
any |
408 |
The request was not received in time: the whole request, its body included, takes more than 60 seconds, or the body arrives slower than 128 bytes per second. |
A client should retry on 503 after the Retry-After seconds, on 408 and on a timeout or a connection error; it should not retry a 4xx without changing the request.
Requests on endpoints that run SQL (LIST, READ and writes without bindings) are forwarded to Core Hub and answered with its status; the HTTP Source adds 502 with the fixed message Upstream error when Core Hub’s answer cannot be used, and 504 when Core Hub does not answer within 35 seconds.
Supported data types
Every binding becomes a column with a fixed type, which the target uses to create or match its column. A value that does not fit the column is refused with 400 instead of being queued, so one bad request can never block the rows behind it.
| Binding type | Column type | Accepted values |
|---|---|---|
STRING |
|
Any text up to 255 characters (counted as code points). An object or an array bound to a |
NUMBER |
|
ASCII decimal numbers, optionally signed, with an exponent, of at most 64 characters, with at most 16 integer digits and 2 decimal places once trailing zeros are removed; nothing is rounded. |
BOOLEAN |
|
|
|
|
The request sequence, present only on endpoints without a key. |
Authentication
Every request carries a Personal API Token: Authorization: Bearer gsp_… (the scheme is matched ignoring case). The token is accepted when:
-
it has the shape of a Personal API Token,
gsp_followed by 64 lowercase hexadecimal digits: any other value is refused at once, without asking Core Hub, and never cached; -
it is valid (not revoked, not expired) and its owner still exists;
-
the role of its owner may write, as in Query Studio: a Manager or a Super Admin. A token of a Viewer or a Monitor is refused with 403, whatever its scopes;
-
it carries the HTTPS agent permission scope (
gluesync.https_agent), or no permission restriction at all; -
its agent scopes include the endpoint’s pipeline and HTTP Source agent, or it has no agent scope at all;
-
the collection’s blocklist and allowlist admit it.
The collection has two lists of token ids, applied on the HTTP Source and on Core Hub’s own inbound routes by the same rule:
-
The blocklist (
blockedTokenIds) holds the tokens that can never call the collection, whether the allowlist is on or off and whatever their scopes. It wins over everything else. Blocking a token leaves its other collections alone, and you can unblock it later. A token cannot be on the blocklist and on the allowlist at once: Core Hub refuses to save such a collection. -
The allowlist (
allowlistEnabledandallowedTokenIds) narrows the other tokens:-
Off (default): every token that passes the checks above and is not blocked can call the collection.
-
On: only the tokens listed can, on the agents their scopes reach. An empty list admits nobody: removing the last token never opens the collection; turn the allowlist off to let every token in.
-
For example, a collection that only the shop webhook token can call, with a retired token locked out for good:
{
"id": "3d0c2f5e-8a71-4b7e-9c55-0e6f1a2b3c4d",
"name": "shop",
"basePath": "shop",
"allowlistEnabled": true,
"allowedTokenIds": ["<id of the shop webhook token>"],
"blockedTokenIds": ["<id of the retired token>"]
}
Core Hub saves a collection only while at least one active token with the HTTPS agent permission, owned by any user, is admitted by its lists and reaches one of its endpoints (a token without agent scope reaches them all): a change that would leave none, such as blocking the last token, is refused with 400.
Verdicts are cached on the agent, keyed by a hash of the token with the pipeline and the agent, never by the token itself:
-
a token that is valid is remembered for at most one minute, and never past the token’s own expiry;
-
a token that is unknown, revoked or expired is remembered for 5 seconds, enough to spare Core Hub a client that retries a revoked token in a loop;
-
a refusal for a missing permission scope, an agent scope or the owner’s role is not remembered: the token is real, its owner may be granted what it lacks a moment later, and the answer is cheap to ask for again.
Core Hub checks the owner’s role every time it validates a token, so the HTTP Source repeats the check at most once a minute for each token, and on every call that reaches Core Hub’s own routes. Revoking or deleting a token, deleting a user or changing a user’s role, and every change made in Courier, clear the caches of all HTTP Sources: a revoked token is refused within about a second.
Backpressure and limits
-
Body: at most
maxBodyKb(1 MB by default), checked on theContent-Lengthbefore the body is read, and while reading a chunked body. -
Concurrency: at most
maxInFlightrequests (64) are served at once; one more gets 503 at once instead of waiting on a pool. The server uses its ownserverThreads(16), separate from Core Hub’s. -
Time: a request is served for 60 seconds at most, its body included, and a body that arrives slower than 128 bytes per second is dropped, so a client that trickles its body cannot hold one of the
maxInFlightslots for long. Both are answered 408. -
Queue per endpoint: beyond
maxBacklogMb(2 GB) of unconfirmed rows for one endpoint, that endpoint answers 503 withRetry-After: 5; the others keep accepting, so one stalled entity does not turn the others away. -
Queue in total: beyond
maxTotalBacklogMb(8 GB) of space in use by the queue files, every endpoint answers 503 withRetry-After: 5. A queue file reuses its space only once it has drained, so under a constant flow the limit can be reached for a moment even with a fast target: the 503s are brief, the file recycles and requests flow again. -
Disk: while the free space of the queue’s directory is below
minFreeDiskMb(1 GB by default) or 5% of the volume, whichever is more, every endpoint answers 503 withRetry-After: 5, so the queue never fails on a full disk halfway through a row. -
Connections:
acceptQueueSize(1024) connections wait in the operating system before being accepted byacceptors(2) threads; idle connections are closed after 30 seconds. -
Delivery: one poll drains up to 10 full batches or one second of rows, and Core Hub polls every 500 ms by default, so delivery keeps up with sustained intake.
The backlog figures are refreshed once a second and after every row appended, so the queue can exceed a limit by very little before the 503s start.
Security
-
Plain HTTP only on loopback, or on your say-so. Without a keystore, tokens and data would travel unencrypted, so the listener does not start on an address other than loopback (
localhost,::1,127.x.x.x) unlessinboundAllowPlainHttpistrue, and then it logs a warning. Either setinboundTlsKeystorePathandinboundTlsKeystorePassword(andinboundTlsKeyAliasif the keystore holds several keys) so the listener serves HTTPS only, or put the listener behind a TLS terminator (reverse proxy, load balancer) and setinboundBindHostto127.0.0.1, or to a private interface together withinboundAllowPlainHttp. The certificate must name the host names or IP addresses the clients use: the server checks the requested host against it and answers 400 otherwise. -
Bind host. The listener binds every interface (
0.0.0.0) by default; restrict it withinboundBindHostand the firewall. -
Credentials never reach the target.
Authorization,Proxy-AuthorizationandCookiecannot be bound, so the caller’s token or session never ends up in a table. -
Least privilege. Give each client its own token with only the HTTPS agent permission and the agent scope of its HTTP Source, owned by a user whose role you would let write, with an expiry. Turn the collection allowlist on to restrict a collection to named tokens, and block a token you retire from one collection without revoking it everywhere.
-
Routes stay hidden. A request that matches no endpoint is answered 401 unless it carries a token the HTTP Source accepts, so the collections and endpoints cannot be listed before authenticating.
-
Nothing sensitive in logs or answers. Refused requests are logged without the body, the values or the token, with control characters stripped. Error answers never carry internal details: not Core Hub’s address, an exception message or a path on disk, and when Core Hub’s own answer cannot be used the client gets the fixed message
Upstream error. -
Slow clients. The time a request may take and the minimum speed of its body are bounded, so clients that trickle bytes cannot turn everyone else away.
-
Proxies. A JSON body that repeats a key is refused, so a proxy or WAF in front that checks the first occurrence cannot be bypassed with a second one.
Monitoring
-
http_source_requests_total{endpoint,status}: requests per endpoint name and status code (unmatchedfor requests matching no endpoint). The status tag tells 202, 400, 401, 403, 404, 408, 413 and 503 apart. -
Entity lag: the rows and bytes queued and not yet confirmed, and the age of the oldest one, are reported as the entity lag (
gluesync_cache_*metrics) on the entity monitoring. -
Read checkpoint: the sequence counter is the agent’s read checkpoint (
gluesync_read_checkpoint_*metrics), saved every second when it moves and every minute otherwise, so its age stays meaningful on an idle agent. -
Logs: refused requests are not logged one by one. One
WARNline per minute sums them up by status code, and a recurring failure is logged in full once a minute, then counted. Per-request details are atDEBUGlevel.
Limitations
-
No deduplication of retried requests. The HTTP Source does not read an
Idempotency-Keyheader: a client that retries a request it already got202for queues a second row. On an endpoint with a key the second row rewrites the same target row; on an append-only endpoint (_seqkey) it is a duplicate. (The HTTP Target does send the header toward the APIs it calls.) -
No delivery status endpoint. The
202confirms the row is queued, not written: follow delivery through the entity monitoring. -
Writes only.
LISTandREADendpoints stay on Core Hub’s SQL path against a SQL-capable source agent; a source cannot answer a read. -
One port per agent instance. Each HTTP Source instance in Core Hub listens on its own
inboundPort. -
Fixed column sizes. Strings up to 255 characters and numbers up to
NUMERIC(18,2); larger values are refused with 400. -
No partial updates.
PATCHis not available: anUpdatereplaces the whole row. -
Ordering holds per endpoint, not across endpoints.
Troubleshooting
| Symptom | What to check |
|---|---|
The HTTP Source does not start with |
No keystore is set and the listener would bind an address other than loopback. Set |
Connection refused on the port |
The HTTP Source is not configured or not started (see the row above), |
409 |
The request was sent to Core Hub’s port: call the HTTP Source port. |
404 on a URL shown in Courier |
The verb differs from the endpoint’s, the base path is wrong, or the HTTP Source’s |
401 |
The header is missing, the scheme is not |
403 |
The token lacks the HTTPS agent permission, is scoped to another pipeline or agent, is owned by a Viewer or a Monitor, is on the collection’s blocklist, or is not on its allowlist while it is on (an empty allowlist admits nobody). The message tells which check failed. |
503 with |
Too many requests at once ( |
503 with |
The target does not confirm rows: check the entity state and the target, then the lag. Raise |
408 |
The request or its body took more than 60 seconds, or the body came in slower than 128 bytes per second: check the client and the network. |
The collection cannot be saved, |
No active token with the HTTPS agent permission is admitted by the collection’s lists and reaches its endpoints. Create a token first, or allow or unblock one. Tokens of every user count, but the Access tokens section lists only yours. |
400 with reasons |
Use Preview the row in Courier with the same values: it shows the same reasons the agent answers. |
Requests answered 202 but nothing on the target |
The entity is stopped or in error: the rows are queued and flow once it runs. A DELETE-only endpoint whose entity has non-key target columns blocks on the first delete: narrow the entity to the key columns. |
HTTPS clients get 400 or a certificate error |
The certificate does not name the host or IP the client calls. |
For anything else, open a ticket on the support portal with the agent log and the http_source_requests_total figures.
Guidelines
-
Give every endpoint a key when the target has one. A key binding (or a required
PATHbinding) makes updates, deletes and client retries address the same row; keep_seqfor append-only targets. -
Use one endpoint with an operation binding when a client sends inserts, updates and deletes of the same resource: its rows carry every column and keep the per-endpoint order.
-
Retry on 503, 408 and on timeouts, not on other 4xx. Honour
Retry-After; a 400 does not change by repeating it. A request that got no202may still have been queued, so make the retry harmless: give the endpoint a key. -
Turn on TLS or put a terminator in front before exposing the port outside a trusted network.
-
Size the disk for outages. The queue absorbs a target outage up to
maxTotalBacklogMb; at your intake rate, that is how long the target can be down before clients get 503. -
Test with Preview the row first, then with
curl, before pointing production clients at the endpoint.