One to one message delivery system.
Built on top of OpenResty and KeyDB, inheriting their scalability and performance.
- Communication between two parties behind NAT
- Converting webhook updates into long polling endpoint
Because you can't be a Tunnelhead without tunnels
- Relay - a software, which repeats the message send by a producer to a consumer
- Tunnel - a one-way path for messages between two parties
- Producer - a party sending a message
- Consumer - a party receiving a message
- Message - a single blob of data sent from one party to another
- Event - a detectable occurrence which causes a message to be sent
- Queue - a list of messages sent by one party, but not yet accepted by a second party
- Backpressure - a mechanism, which production of new messages when queue is congested
- Pending - a message, which was accepted by a second party, but not yet processed
This software is designed to work in a Docker container. OpenResty DNS resolver is hardcoded to use Docker.
Dockerfile is provided for a standalone setup.
Example usage (requies having docker container with redis in docker network my-network):
git clone https://github.com/tunnelhead/http-event-relay.git
cd http-event-relay
docker build -t http-event-relay .
docker run -p 8080:80 -e REDIS_HOST=my-redis-instance --network=my-network --name test-relay http-event-relay
curl -s http://localhost:8080/health
Various docker compose files are provided for easy development and testing.
Docker compose configuration includes both keydb (redis alternative) and relay containers.
Example usage:
git clone https://github.com/tunnelhead/http-event-relay.git
cd http-event-relay
docker-compose up -d
docker run --rm --network http-event-relay_default alpine/curl -s http://tunnel-server/health
Development:
For relay development purposes, a separate docker compose configuration is provided.
It disables lua cache and mounts the sources directory to the container, which acts as a "hot reload".
Once container is running, any changes in src directory will be reflected immediately.
git clone https://github.com/tunnelhead/http-event-relay.git
cd http-event-relay
docker-compose -f 'docker-compose.dev.yml' up -d
curl http://localhost:8080/health
curl -v -H "Authorization: Bearer thisisasecret" http://localhost:8080/t/my-secret-tunnel
Development version has demo enabled by default http://localhost:8080/demo/
Tests:
When development containers are up and running, you can execute some automatic tests to validate the changes.
NodeJs is required to run the tests.
cd tests
npm install
npm run tests
Event relay can be configured using the following environmental variables:
| Env var | Description | Default |
|---|---|---|
| REDIS_HOST | Redis hostname | 127.0.0.1 |
| REDIS_PORT | Redis port | 6379 |
| REDIS_PASSWORD | Redis password (optional, use if required) | |
| REDIS_POOL_SIZE | How much connections to keep alive after use | 100 |
| REDIS_POOL_KEEPALIVE | How long to keep connections alive after use (seconds) | 10 |
| TUNNEL_ACCESS_TOKEN | Access token to authenticate requests to the relay | |
| TUNNEL_SIGNATURE_SECRET | Secret for validating request data signature | |
| TUNNEL_PUBLIC_IDS | Only allow these tunnels without auth, comma-separated | |
| TUNNEL_MAXLEN | Maximum queue size for a single tunnel | 1000 |
| TUNNEL_BACKPRESSURE | If backpressure should be enabled by default (1 or 0) | 1 |
| TUNNEL_MAX_POLL_TIMEOUT | Maximum wait time for long polling (seconds) | 60 |
| TUNNEL_DEFAULT_POLL_TIMEOUT | Default wait time for long polling (seconds) | 30 |
| TUNNEL_DEFAULT_CONTENT_TYPE | Content-Type header to use if not provided by producer | text/plain |
| TUNNEL_REPLY_TTL | How much seconds to keep a message reply | 3600 |
Max message size is set to 128kb by default using client_max_body_size option in nginx.conf.
By default relay server is not protected and accepts any requests. One or more options can be configured to protect it.
If access token is provided in TUNNEL_ACCESS_TOKEN configuration option,
requests to tunnel endpoints (/t/...) will require authorization header:
Authorization: Bearer <access-token>
Producers can be protected via payload signature validation instead of the access token.
HMAC secret must be provided in TUNNEL_SIGNATURE_SECRET configuration option,
requests to producer endpoints will require valid signature in the header (e.g. for GitHub Webhooks):
X-Hub-Signature-256: sha256=<signature>
Multiple predefined tunnel IDs can be provided in the TUNNEL_PUBLIC_IDS configuration option (comma-separated).
If this option is used, provided tunnels will be available without authorization regardless of other auth options. Additionally, any other tunnels become unavailable, unless another auth option is configured to access them.
This can be used to protect access to the relay if producer (e.g. webhook source) doesn't support any auth methods. In this case, long unique tunnel id can be configured as public.
Example:
TUNNEL_PUBLIC_IDS="6962add5-bdf9-4e6d-b2fc-53efa1c4897d,58e1a750-8bfa-40e6-8faa-eeec58e7b8f2"
GET /t/6962add5-bdf9-4e6d-b2fc-53efa1c4897d - allowed
GET /t/58e1a750-8bfa-40e6-8faa-eeec58e7b8f2 - allowed
GET /t/something-else - forbidden without access token
Protocol description mentions <tunnel-id> in urls, which must be replaced with user-selected identifier.
It must match on the producer and the receiver side for the message to be delivered.
It's advised to use UUIDv4 or similar large random unique identifier for tunnel id, especially in public networks.
Acceptable characters in the identifier:
- Any letter
- Any digit
- Characters '-' and '_'
Tunnel IDs can be up to 1024 characters long.
Tunnel IDs are case-sensitive, "test" and "Test" are two different tunnels.
A single tunnel provides one-way communiation between two parties (from a producer to a consumer) with optional replies on acknowledgement.
For robust two-way communication, use two tunnels with different ids (one in each direction).
With the default configuration, backpressure is enabled by default.
This means that if producer sends messages faster than consumer is able to read them, producer will start receiving errors from the relay once queue size limit is reached.
This behaviour can be disabled in configuration or per tunnel on the producer side. In this case, if consumer is unable to keep up and queue size limit is reached, oldest messages can be discarded even if they are not yet read by the consumer.
It makes sense to disable backpressure if optimising for delivery speed and data loss is acceptable.
By default the consumed message is automatically acknowledged and deleted.
However, if using pending mode on the consumer, the relay will keep returning the same message over and over again, until it's manually acknowledged.
Alternatively, it's possible to acknowledge with reply to send some information back to the producer. Producer then must read or poll for the reply.
Replies are stored for limited time (as configured) and removed immediately after read. If more reliable way is required, consider using a separate tunnel for communication in other direction.
- Common Placeholders
- Common Errors
- 1. Produce a Message
- 2. Consume Messages
- 3. Acknowledge a Message
- 4. Acknowledge with Reply
- 5. Read Message Reply
- 6. Get Message Status
- 7. Get Queue Size
- 8. Clear Tunnel (Delete All Messages)
- 9. Health Check
<tunnel-id>: A unique string identifying a specific tunnel.<message-id>: A unique string identifying a specific message within a tunnel.
Generally, these errors can occur on any endpoint.
| Status Code | Description |
|---|---|
400 Bad Request |
Invalid URL or parameter provided. |
500 Internal Server Error |
Internal error occurred. The response body contains an error description. See OpenResty log for more details. |
POST /t/<tunnel-id>
Stores a message in the specified tunnel. The Content-Type header of the request is stored with the message and sent to the consumer.
| Parameter | Type | Description | Required | Default |
|---|---|---|---|---|
limit |
Integer | Sets the maximum queue size for backpressure. A value of 0 disables backpressure. |
No | As configured |
| Header | Description | Example | Required | Default |
|---|---|---|---|---|
Content-Type |
The format of the message body. | application/json |
No | As configured |
| Header | Description | Example |
|---|---|---|
X-Message-Id |
The ID of the created message. | 1746450313373-0 |
X-Queue-Size |
Current queue size (returned if backpressure is enabled via limit). |
1 |
Success:
| Status Code | Description |
|---|---|
201 Created |
Message successfully produced. Empty body. |
Errors:
| Status Code | Description |
|---|---|
507 Insufficient Storage |
Backpressure enabled and queue size limit reached. X-Queue-Size header indicates the current queue size. |
Request:
curl -d '{"text": "Hello, World!"}' \
-H "Content-Type: application/json" \
-X POST https://relay.tunnelhead.dev/t/demo?limit=100Response:
HTTP/1.1 201 Created
X-Message-Id: 1746450313373-0
X-Queue-Size: 1GET /t/<tunnel-id>
Checks for new messages in the tunnel in a non-blocking manner. The request completes immediately.
| Parameter | Type | Description | Required |
|---|---|---|---|
pending |
Flag | If present, the consumed message is not automatically deleted and must be acknowledged manually. | No |
| Header | Description | Example |
|---|---|---|
Content-Type |
The format of the message body. | application/json |
X-Message-Id |
The ID of the consumed message. | 1746450313373-0 |
Success:
| Status Code | Description |
|---|---|
200 OK |
New message found. The response body contains the message. Content-Type and X-Message-Id are present. |
204 No Content |
No new messages were found. Empty body. |
Request:
curl -v https://relay.tunnelhead.dev/t/demoResponse (if message exists):
HTTP/1.1 200 OK
X-Message-Id: 1746450313373-0
Content-Type: application/json
Content-Length: 25
{"text": "Hello, World!"}Response (if no message):
HTTP/1.1 204 No ContentGET /t/<tunnel-id>/poll
Checks for new messages in the tunnel in a blocking manner. The request will complete when a new message appears or if a timeout is reached, whichever comes first.
| Parameter | Type | Description | Required | Default |
|---|---|---|---|---|
timeout |
Integer | Timeout in seconds. If larger than the max timeout configured for the relay instance, max timeout is used. | No | As configured |
pending |
Flag | If present, the consumed message is not automatically deleted and must be acknowledged manually. | No | N/A |
Both timeout and pending can be used together: GET /t/<tunnel-id>/poll?timeout=10&pending
(Same as non-blocking consume: Content-Type, X-Message-Id)
Success:
| Status Code | Description |
|---|---|
200 OK |
New message found. The response body contains the message. Content-Type and X-Message-Id are present. |
204 No Content |
No new messages were found within the timeout period. Empty body. |
Request:
curl -v "https://relay.tunnelhead.dev/t/demo/poll?timeout=10"Response (if message arrives within 10s):
HTTP/1.1 200 OK
X-Message-Id: 1746450313373-1
Content-Type: application/json
Content-Length: 25
{"text": "Another message"}Response (if timeout occurs):
HTTP/1.1 204 No ContentDELETE /t/<tunnel-id>/<message-id>
This endpoint is used by consumers in pending mode to acknowledge message delivery and delete it from the tunnel.
Success:
| Status Code | Description |
|---|---|
204 No Content |
Message acknowledged and deleted. Empty body. |
Request:
curl -X DELETE https://relay.tunnelhead.dev/t/demo/1746483376267-0Response:
HTTP/1.1 204 No ContentPOST /t/<tunnel-id>/<message-id>/reply
This endpoint acknowledges a message and stores the reply for the producer to read.
The Content-Type header of the request is stored with the reply and sent back to the producer.
| Header | Description | Example | Required | Default |
|---|---|---|---|---|
Content-Type |
The format of the message body. | application/json |
No | As configured |
Success:
| Status Code | Description |
|---|---|
201 Created |
Reply successfully saved. Empty body. |
Errors:
| Status Code | Description |
|---|---|
204 No Content |
Message to reply to is not found or not in pending state. |
Request:
curl -d '{"text": "Hello, World!"}' \
-H "Content-Type: application/json" \
-X POST https://relay.tunnelhead.dev/t/demo/1746483376267-0/replyResponse:
HTTP/1.1 201 CreatedGET /t/<tunnel-id>/<message-id>/reply
This endpoint allows producer to read a reply to a message, if a consumer has sent one during acknowledgement in pending mode.
| Header | Description | Example |
|---|---|---|
Content-Type |
The format of the message body. | application/json |
Success:
| Status Code | Description |
|---|---|
200 OK |
Reply found. The response body contains the message. Content-Type is present. |
204 No Content |
No reply was found. Empty body. |
Request:
curl -v https://relay.tunnelhead.dev/t/demo/1746483376267-0/replyResponse (if reply exists):
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 25
{"text": "Hello, World!"}Response (if no reply):
HTTP/1.1 204 No ContentGET /t/<tunnel-id>/<message-id>/reply/poll
This endpoint allows producer to wait for a reply to a message (in a blocking manner).
| Parameter | Type | Description | Required | Default |
|---|---|---|---|---|
timeout |
Integer | Timeout in seconds. If larger than the max timeout configured for the relay instance, max timeout is used. | No | As configured |
| Header | Description | Example |
|---|---|---|
Content-Type |
The format of the message body. | application/json |
Success:
| Status Code | Description |
|---|---|
200 OK |
Reply found. The response body contains the message. Content-Type is present. |
204 No Content |
No reply was found within the timeout period. Empty body. |
Request:
curl -v "https://relay.tunnelhead.dev/t/demo/1746483376267-0/reply?timeout=10"Response (if reply arrives within 10s):
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 25
{"text": "Another message"}Response (if timeout occurs):
HTTP/1.1 204 No ContentGET /t/<tunnel-id>/<message-id>
Retrieves the status of a message in a tunnel.
Success:
| Status Code | Description |
|---|---|
201 Created |
Message was created by producer, but not seen by consumer yet |
202 Accepted |
Message was seen by consumer, but not acknowledged yet (pending mode) |
204 No Content |
Message never existed or was already consumed and acknowledged |
Request:
curl -v https://relay.tunnelhead.dev/t/demo/1746483376267-0Response:
HTTP/1.1 202 AcceptedGET /t/<tunnel-id>/len
Retrieves the current size of the message queue for a tunnel. This is particularly useful when backpressure is enabled. The queue size includes both seen (pending) and unseen messages.
| Header | Description | Example |
|---|---|---|
X-Queue-Size |
Current queue size. | 3 |
Success:
| Status Code | Description |
|---|---|
204 No Content |
Success. Current queue size is in the X-Queue-Size header. Empty body. |
Request:
curl -v https://relay.tunnelhead.dev/t/demo/lenResponse:
HTTP/1.1 204 No Content
X-Queue-Size: 3DELETE /t/<tunnel-id>/all
Clears all messages from the specified tunnel, including pending and not-yet-seen messages. This effectively deletes the tunnel and its contents.
| Header | Description | Example |
|---|---|---|
X-Queue-Size |
Current queue size (will always be 0). |
0 |
Success:
| Status Code | Description |
|---|---|
204 No Content |
Tunnel cleared. X-Queue-Size header indicates 0. Empty body. |
Request:
curl -X DELETE https://relay.tunnelhead.dev/t/demo/allResponse:
HTTP/1.1 204 No Content
X-Queue-Size: 0GET /health
This endpoint can be used to verify that the OpenResty service is up and running.
Success:
| Status Code | Description |
|---|---|
200 OK |
Service is healthy. Body contains "OK". |
Request:
curl https://relay.tunnelhead.dev/healthResponse:
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 2
OKWIP