Search docs.molo17.com

    Gluesync Courier logo 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 (LIST, READ) and write endpoints without bindings keep running SQL through Core Hub against a SQL-capable source agent, as before. Only write endpoints with bindings, assigned to an HTTP Source, become pipeline endpoints.

    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
    1. Limits. A body larger than maxBodyKb is refused with 413, a request beyond maxInFlight with 503, and a request not received within 60 seconds with 408: the server never queues work it cannot bound.

    2. 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.

    3. Authentication. The Personal API Token in the Authorization: Bearer header 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.

    4. 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, Update or Delete.

    5. 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.

    6. 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 202 means 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 gets 503, and the row may be queued anyway. A request that got no 202 (a timeout, a connection reset, a 503, 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 202 is 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 got 202 for queues it twice.

    • Ordered per endpoint. Rows of one endpoint are delivered in the order their 202 sequences 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 (8090 by 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 inboundAllowPlainHttp is 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 the http_cache directory under the agent’s data directory, and minFreeDiskMb (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

    inboundPort (Listening port)

    8090

    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.

    inboundBindHost (Listening address)

    0.0.0.0

    Interface the listener binds. Use 127.0.0.1 when a TLS terminator on the same host is the only client. Without a keystore, an address other than loopback (localhost, ::1, 127.x.x.x) is refused unless inboundAllowPlainHttp is true.

    inboundCollection (Inbound collection)

    empty (all)

    Name, id or base path of the only inbound collection this instance serves. Empty: every collection holding pipeline endpoints of this agent.

    maxBodyKb (Largest body (KB))

    1024

    Largest request body. A larger Content-Length is refused with 413 before the body is read, and so is a chunked body that grows beyond it.

    maxInFlight (Requests served at once)

    64

    Requests being authenticated, bound and queued at the same time. One more gets 503 with Retry-After: 1 instead of waiting.

    maxBacklogMb (Unconfirmed queue limit per endpoint (MB))

    2048

    Bytes queued for one endpoint and not yet confirmed by the target. Beyond it new requests on that endpoint get 503 with Retry-After: 5; the other endpoints keep accepting.

    maxTotalBacklogMb (Unconfirmed queue limit in total (MB))

    8192

    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 Retry-After: 5 until the files drain and recycle.

    serverThreads (Server threads)

    16

    Threads serving requests (at most 512). They are separate from Core Hub’s own threads.

    acceptQueueSize (Pending connections queue)

    1024

    Connections the operating system holds before the server accepts them (at most 65535). Raise it for bursts of new connections.

    acceptors (Connection acceptors)

    2

    Threads accepting new connections (at most 8).

    inboundTlsKeystorePath (TLS keystore path)

    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 inboundAllowPlainHttp.

    inboundTlsKeystorePassword (TLS keystore password)

    empty

    Password of the keystore and of its private key. Required when a keystore path is set.

    inboundTlsKeyAlias (TLS key alias)

    empty (first private key)

    Alias of the key to serve.

    inboundAllowPlainHttp (Allow plain HTTP beyond loopback)

    false

    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. true is the explicit consent to serve plain HTTP anyway, for a deployment whose TLS is terminated before the agent; the agent then logs a warning. Only true consents: anything else, a typo included, keeps the default.

    minFreeDiskMb (Free disk to keep (MB))

    1024

    Free space the queue’s directory must keep, or 5% of the volume if that is more. Below it new requests get 503 with Retry-After: 5, so a full disk never stops the queue halfway through a row.

    Values that are not positive integers fall back to their default.

    Courier: define the pipeline endpoints

    In Courier  Inbound 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 SQL is the existing SQL endpoint.)

      • Endpoint name: the source table the pipeline will see.

      • Operation and HTTP method: CREATE with POST, UPDATE with PUT, DELETE with DELETE. PATCH is 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) or BODY (a body field);

        • Type: STRING, NUMBER or BOOLEAN, 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 PATH bindings, or the request sequence _seq when there are none.

      • Operation binding (optional): a STRING binding whose value, Insert, Update or Delete (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) or FORM_URLENCODED, for the BODY bindings of POST and PUT endpoints.

    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
    • PATH values come from the placeholders, QUERY values from the query string (the first value of a repeated parameter), HEADER values from the headers, matched ignoring case.

    • A JSON body must be an object. A BODY binding named customer.address.city reads that key if it exists as is, otherwise walks the nested objects customer → address → city. An object or an array bound to a binding is stored as its raw JSON text.

    • A FORM_URLENCODED body 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 JSON null stays null even when the binding has a default.

    • Every row carries every binding, so an Update replaces the target row as a PUT does: a binding the client did not send is written as its default or null.

    • An endpoint without key bindings and without required PATH bindings gets a synthetic _seq key 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 READ and LIST endpoints 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, or x@.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. _seq is reserved.

    • Every {placeholder} of the path has a required PATH binding of the same name, and every PATH binding has its placeholder.

    • Key bindings are declared and required.

    • UPDATE and DELETE endpoints need a key (a key binding or a required PATH binding), and so does any endpoint with an operation binding: with the _seq key an update or a delete would find no row and change nothing.

    • The operation binding is declared and of type STRING.

    • PATCH is refused; the verb must fit the operation (POST for CREATE, PUT for UPDATE, DELETE for DELETE).

    • BODY bindings only on POST and PUT, with a body type other than NONE.

    • A HEADER binding has a valid header name and is not Authorization, Proxy-Authorization or Cookie: 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 as Authorization: Bearer <token>.

    • The Allowlist toggle (Off · any token within its scopes or On · 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 202 and 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.

    Insert an order
    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.

    Delete the same order through the operation binding
    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}'
    A request the binding refuses
    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 (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS).

    2

    413

    The Content-Length is larger than maxBodyKb.

    3

    503

    maxInFlight requests are already being served.

    1

    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 Authorization: Bearer header, or a value that is not a Personal API Token, or a token that is unknown, revoked or expired. The answer carries WWW-Authenticate: Bearer.

    5

    403

    The token lacks the HTTPS agent permission scope (gluesync.https_agent), is not scoped to the endpoint’s pipeline and HTTP Source agent, belongs to a user whose role may not write (a Viewer or a Monitor), is on the collection’s blocklist, or is not on its allowlist while the allowlist is on.

    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.

    1

    6

    503

    The free space of the queue’s disk is below minFreeDiskMb (or 5% of the volume), the endpoint’s unconfirmed queue is above maxBacklogMb, or the agent’s queue files are above maxTotalBacklogMb.

    5

    7

    413

    The body read is larger than maxBodyKb (a chunked body without Content-Length).

    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 Insert, Update or Delete. The body lists every reason.

    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).

    1

    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 503 if Core Hub could not tell.

    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.

    1

    13

    202

    The row is queued and on the disk: {"sequence": …, "collection": …, "endpoint": …, "operation": …}.

    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

    VARCHAR(255)

    Any text up to 255 characters (counted as code points). An object or an array bound to a BODY binding arrives as its raw JSON text and must fit too.

    NUMBER

    NUMERIC(18,2)

    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

    BOOLEAN

    true or false, ignoring case and surrounding spaces.

    _seq (synthetic key)

    NUMERIC(18,0)

    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 (allowlistEnabled and allowedTokenIds) 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 the Content-Length before the body is read, and while reading a chunked body.

    • Concurrency: at most maxInFlight requests (64) are served at once; one more gets 503 at once instead of waiting on a pool. The server uses its own serverThreads (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 maxInFlight slots for long. Both are answered 408.

    • Queue per endpoint: beyond maxBacklogMb (2 GB) of unconfirmed rows for one endpoint, that endpoint answers 503 with Retry-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 with Retry-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 with Retry-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 by acceptors (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) unless inboundAllowPlainHttp is true, and then it logs a warning. Either set inboundTlsKeystorePath and inboundTlsKeystorePassword (and inboundTlsKeyAlias if the keystore holds several keys) so the listener serves HTTPS only, or put the listener behind a TLS terminator (reverse proxy, load balancer) and set inboundBindHost to 127.0.0.1, or to a private interface together with inboundAllowPlainHttp. 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 with inboundBindHost and the firewall.

    • Credentials never reach the target. Authorization, Proxy-Authorization and Cookie cannot 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 (unmatched for 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 WARN line 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 at DEBUG level.

    Limitations

    • No deduplication of retried requests. The HTTP Source does not read an Idempotency-Key header: a client that retries a request it already got 202 for queues a second row. On an endpoint with a key the second row rewrites the same target row; on an append-only endpoint (_seq key) it is a duplicate. (The HTTP Target does send the header toward the APIs it calls.)

    • No delivery status endpoint. The 202 confirms the row is queued, not written: follow delivery through the entity monitoring.

    • Writes only. LIST and READ endpoints 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. PATCH is not available: an Update replaces the whole row.

    • Ordering holds per endpoint, not across endpoints.

    Troubleshooting

    Symptom What to check

    The HTTP Source does not start with Refusing to serve the HTTP Source over plain HTTP on …

    No keystore is set and the listener would bind an address other than loopback. Set inboundTlsKeystorePath and inboundTlsKeystorePassword, bind inboundBindHost to 127.0.0.1 behind a TLS terminator, or set inboundAllowPlainHttp to true if TLS ends before the agent.

    Connection refused on the port

    The HTTP Source is not configured or not started (see the row above), inboundPort differs, the port is not published from the container or is closed in the firewall, or inboundBindHost limits the interface. A port already in use stops the agent with Cannot start the HTTP Source: port … already in use on ….

    409 This endpoint is served by its HTTP Source agent, not by CoreHub

    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 inboundCollection limits it to another collection (Courier warns about it). A 404 No entity of the pipeline consumes endpoint … means no entity reads the endpoint yet. Without a valid token the same request gets 401, not 404.

    401

    The header is missing, the scheme is not Bearer, the token was revoked or expired, or it was copied incompletely.

    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 Retry-After: 1

    Too many requests at once (maxInFlight), Core Hub unreachable on a token cache miss, or the queue not open yet.

    503 with Retry-After: 5 lasting

    The target does not confirm rows: check the entity state and the target, then the lag. Raise maxBacklogMb / maxTotalBacklogMb only if the disk allows it. With the message The disk of the queue is almost full the free space is below minFreeDiskMb or 5% of the volume: free some space.

    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, needs an active Personal API Token

    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 PATH binding) makes updates, deletes and client retries address the same row; keep _seq for 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 no 202 may 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.