Internet-Draft M. Toomim
Expires: 6 May 2027 Invisible College
Intended status: Proposed Standard 2 November 2026
Poll and Push Modes for HTTP Subscriptions
draft-toomim-httpbis-braid-poll-and-push-00
Abstract
In the stream mode of Braid-HTTP subscriptions, the server holds one
response open, and sends each change in it. Some origin servers
can't hold a response open, such as those that run as serverless
functions or on shared hosting. And on some paths, a proxy buffers
each response until it ends. This document defines two subscription
modes in which nothing stays open for long. In the long-poll mode,
the client sends a GET for each change, and the server responds to it
when the change happens. In the webhook mode, the origin server
pushes each change to a subscription gateway as a PUT or a DELETE,
and the gateway serves the subscription to its clients. In both,
each message after the first says which version it builds on, and the
first can too, so no message depends on a connection, and each can
take its own path.
This document is one of the Braid-HTTP extensions, which work alone
or together; together they make HTTP a state synchronization
protocol.
About This Document
This note is to be removed before publishing as an RFC.
Discussion of this document takes place on the HTTP Working Group
mailing list (ietf-http-wg@w3.org). The source of this document, and
an issue tracker, are at https://github.com/braid-org/braid-spec.
Status of this Memo
This Internet-Draft is submitted in full conformance with the
provisions of BCP 78 and BCP 79.
Internet-Drafts are working documents of the Internet Engineering
Task Force (IETF). Note that other groups may also distribute
working documents as Internet-Drafts. The list of current
Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.
Internet-Drafts are draft documents valid for a maximum of six months
and may be updated, replaced, or obsoleted by other documents at any
time. It is inappropriate to use Internet-Drafts as reference
material or to cite them other than as "work in progress."
This Internet-Draft will expire on 6 May 2027.
Copyright Notice
Copyright (c) 2026 IETF Trust and the persons identified as the
document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info) in effect on the date of
publication of this document. Please review these documents
carefully, as they describe your rights and restrictions with respect
to this document. Code Components extracted from this document must
include Revised BSD License text as described in Section 4.e of the
Trust Legal Provisions and are provided without warranty as described
in the Revised BSD License.
Table of Contents
1. Introduction ...................................................3
1.1. Terminology ..................................................4
2. Subscribe: long-poll ...........................................5
2.1. A poll response is a heartbeat ...............................7
2.2. A poll response has an Origin-Status header ..................7
2.3. Either side can end a long-poll subscription .................8
3. Subscribe: webhook .............................................8
3.1. The gateway asks the origin server to push to it .............9
3.2. The gateway re-sends its subscription request ...............10
3.3. Either side can end a push subscription .....................10
3.4. Short streams can redirect to gateways ......................11
3.5. Verifying the subscription gateway ..........................12
4. Implementation Status .........................................12
5. Security Considerations .......................................13
6. IANA Considerations ...........................................13
7. References ....................................................14
7.1. Normative References ........................................14
7.2. Informative References ......................................15
1. Introduction
A subscription in the stream mode [SUBSCRIPTIONS] needs a response
held open, and not every server or path can hold one. For example,
imagine a newspaper's website on election night, as in
[SUBSCRIPTIONS], with a results page that changes all evening. The
examples in this document follow two cases from that night. A reader
at work sits behind a proxy that buffers each response until it ends,
so her browser never sees the first byte of a stream. And the
county's election office posts the official results from shared
hosting, which ends any response that runs for more than 30 seconds.
This document defines a mode for each case. In the long-poll mode
(Section 2), the reader's browser sends a GET for each change, and
the server holds each one until the change happens. In the webhook
mode (Section 3), the origin server pushes each change to a
subscription gateway, as a request, and the gateway serves the
subscription to its own clients.
In both modes, the server keeps every promise that it makes when it
grants a subscription. The client gets a catch-up, and then each
change ([SUBSCRIPTIONS], Section 2), and it learns from heartbeats
and origin status whether it is caught up ([SUBSCRIPTIONS], Section
5). Each exchange picks up the subscription from the client's
version, named in a Parents header [VERSIONS]. So no exchange
depends on a connection staying open, and since HTTP is stateless
(Section 3.3 of [RFC9110]), each one can take its own path.
The webhook mode generalizes three existing designs. In GRIP [GRIP],
a proxy at the resource's own URL holds the subscriptions for its
origin server. In Mercure [MERCURE], a hub at its own URL relays
each update from a publisher to its subscribers. And in WebSub
[WEBSUB], a hub pushes each update to a subscriber's own URL. A
subscription gateway can sit in any of these places (Section 3).
This document extends Subscriptions [SUBSCRIPTIONS] with these two
modes. The long-poll mode needs Versions [VERSIONS], to name the
version that each poll resumes from, and the webhook mode needs
Updates [UPDATES], since each push is an update.
1.1. Terminology
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in BCP
14 [RFC2119] [RFC8174] when, and only when, they appear in all
capitals, as shown here.
This document uses these terms from Subscriptions [SUBSCRIPTIONS]:
subscription, catch-up, caught up, hop, and origin status. It uses
subscription key from Subscriptions Caching [SUBSCRIPTIONS-CACHING],
Version, Parents, and current version from Versions [VERSIONS], and
update, snapshot, patch, and deletion from Updates [UPDATES]. It
also uses these terms:
Poll: One GET in a long-poll subscription.
Subscription gateway: A server that holds subscriptions for an
origin server, asks the origin server to push each change to it,
and serves those changes to its own clients.
Push subscription: A subscription in the webhook mode, from a
subscription gateway to an origin server.
Push: A request in which the origin server sends one change of a
push subscription to the gateway.
Push URL: The URL that an origin server sends a push subscription's
pushes to.
2. Subscribe: long-poll
In the long-poll mode, the client sends a GET, which we call a poll,
for each response, and the server holds each poll until it has
something to send. For example, the reader at work never sees the
first byte of her stream, so her browser subscribes again, with
long-poll:
Request:
GET /election
Subscribe: long-poll
Heartbeats: 30
Response:
HTTP/1.1 200 OK
Subscribe: long-poll
Heartbeats: 30
Version: ("2")
Origin-Status: up
Cache-Control: max-age=0, stale-if-error=86400
Cache-Control: no-cache="Origin-Status"
Content-Type: text/plain
Content-Length: 21
Bob leads, 51% to 49%
The server responds to the first poll at once, with the catch-up, as
it would to an ordinary GET. With "Subscribe: long-poll" in its
response, the server grants the subscription. Then the browser sends
its next poll, with Parents set to the version it now holds, and the
server holds that poll until the results change:
Request:
GET /election
Subscribe: long-poll
Heartbeats: 30
Parents: ("2")
Origin-Status: up
Response:
HTTP/1.1 200 OK
Subscribe: long-poll
Heartbeats: 30
Version: ("3")
Parents: ("2")
Origin-Status: up
Cache-Control: max-age=0, stale-if-error=86400
Cache-Control: no-cache="Origin-Status"
Content-Type: text/plain
Content-Length: 20
Bob wins, 51% to 49%
Then the browser polls again, from version 3. Once a client knows
its current version ([VERSIONS], Section 2.2.3), each of its polls
MUST have a Parents header naming it, since the server responds to
each poll with what changed since that version. Parents names the
client's whole current version, so a change on another branch of the
history reaches the client too.
The server responds to each poll as it would to an ordinary GET with
the same headers, with the response's own Cache-Control. A cache
stores the responses to polls as it would any response. If several
changes have happened since the poll's version, the server can
respond with one snapshot, or with a multiresponse [MULTIRESPONSE]
that holds each change, since a poll's Subscribe header says that the
client can read one.
2.1. A poll response is a heartbeat
A client asks for heartbeats with the Heartbeats header, as in the
stream mode ([SUBSCRIPTIONS], Section 5.1), and in its response, the
server gives the interval that it will use. In the long-poll mode,
the server responds to each poll within that interval. If nothing
has changed by the time the interval runs out, it responds with a 304
(Not Modified), as it would to any GET whose Parents is the current
version ([VERSIONS], Section 2.3.5). For an ordinary GET, the server
sends that 304 at once. For a poll, it holds the 304 until the
interval runs out, and responds sooner with the change, if one
happens.
So each response is a heartbeat. If no response comes within the
interval, plus an allowance for network delay, the client treats the
hop as dead, as in the stream mode, and polls again from its current
version.
A server ignores If-None-Match and If-Modified-Since on a poll, as on
any subscription ([SUBSCRIPTIONS], Section 3.3), since the client
already names the version it holds, in its Parents header.
2.2. A poll response has an Origin-Status header
In the stream mode, the server sends origin status in 105 (Origin
Status) sub-responses ([SUBSCRIPTIONS], Section 5.2). In the
long-poll mode, it sends origin status in the headers of each
response: an Origin-Status header, with its age on a down, and a
Proxy-Status header on a down. For example, while the reader's poll
from version 2 waits, the paper's CDN stops hearing from the origin
server at 03:20, and its own subscription times out at 03:20:30. The
CDN responds to the poll at once:
HTTP/1.1 304 Not Modified
Subscribe: long-poll
Heartbeats: 30
Version: ("2")
Origin-Status: down;age=30
Proxy-Status: ExampleCDN; error=connection_read_timeout
Cache-Control: max-age=0, stale-if-error=86400
Cache-Control: no-cache="Origin-Status"
In each poll, the client says which status it last heard, with an
Origin-Status header of its own. When the status changes, the server
responds to the poll at once, with a 304 if nothing else has changed,
so the client hears of the change without waiting for the interval to
run out. The client needs only the current status, so it may not
hear of a down and an up that both happen between two of its polls.
Each server sets the Origin-Status of what it sends, as caches set
the Age header (Section 5.1 of [RFC9111]). So each response to a
poll, 304s included, has a Cache-Control header with the directive
no-cache="Origin-Status" (Section 5.2.2.4 of [RFC9111]), so that a
cache doesn't send a stored Origin-Status to a later client. As in
the stream mode, a server sends origin status only on a subscription
with heartbeats ([SUBSCRIPTIONS], Section 5).
2.3. Either side can end a long-poll subscription
The client ends the subscription by not sending its next poll, or by
canceling the poll in flight. If a cache serves long-poll
subscribers, it treats one that stops polling as one that has left
([SUBSCRIPTIONS-CACHING], Section 5.3).
The server ends the subscription by responding to a poll without a
Subscribe header, as it would to any GET for which it grants no
subscription. It can say why in that response, as with a 503
(Service Unavailable) and a Retry-After header.
3. Subscribe: webhook
In the webhook mode, the origin server sends each change as a request
of its own, to a push URL. Each push is an update, sent as a request
([UPDATES], Section 2): a PUT for a snapshot, a PUT with Patch-Type
or Content-Range for a patch, and a DELETE for a deletion. So the
origin server pushes only updates. If a response has no matching
request, such as a 403 (Forbidden), the origin server sends it in
response to one of the gateway's subscription requests (Section 3.2).
The origin server sends the pushes to a subscription gateway, which
holds subscriptions for the origin server, and serves each change to
its own clients, in the stream or long-poll mode, as a cache does
([SUBSCRIPTIONS-CACHING], Section 5). A gateway can sit in any of
three places:
- On the path, at the resource's own URL, as a CDN or a reverse
proxy does. Clients reach it without knowing that it is there,
as in GRIP [GRIP].
- At a URL of its own, as a Mercure hub [MERCURE] does. Clients
reach it through a handoff from the origin server (Section 3.4).
- At the requester itself, as a WebSub subscriber does [WEBSUB],
when the requester can receive requests.
A gateway sees the content of the responses it serves, and holds the
credentials it forwards, so its clients and the origin server trust
it as they would any proxy (Section 5).
3.1. The gateway asks the origin server to push to it
A gateway asks for a push subscription with a GET, naming its push
URL in the "to" parameter of the webhook mode. For example, the
county's hosting provider runs a gateway on the path to the county's
site. The gateway subscribes for the results like this:
Request:
GET /results
Host: votes.county.example
Subscribe: webhook;to="https://votes.county.example/push/7f3a"
Response:
HTTP/1.1 200 OK
Subscribe: webhook;to="https://votes.county.example/push/7f3a"
Version: ("2")
Content-Type: text/plain
Content-Length: 21
Bob leads, 51% to 49%
The origin server grants the push subscription by echoing the
Subscribe header, and its response holds the catch-up. Then it
pushes each later change to the push URL:
PUT /push/7f3a
Host: votes.county.example
Version: ("3")
Parents: ("2")
Content-Type: text/plain
Content-Length: 20
Bob wins, 51% to 49%
The gateway responds to each push with a 200 (OK) or a 204 (No
Content), and serves the change to its clients. It gives each push
subscription a push URL of its own, so each push's URL identifies its
subscription. The origin server identifies a push subscription by
its subscription key and its push URL, so that it can tell a re-sent
request (Section 3.2) from a new subscription.
The webhook mode has one parameter, "to", a String that holds the
push URL. A server ignores a webhook mode that has no "to"
parameter, since the server has nowhere to push.
3.2. The gateway re-sends its subscription request
Between pushes, nothing connects the gateway to the origin server.
So the gateway re-sends its subscription request from time to time,
at a rate it picks, with Parents set to its current version. It
names that rate in a Heartbeats header ([SUBSCRIPTIONS], Section
5.1). If the origin server gets no re-sent request within that
interval, plus an allowance for network delay, it ends the push
subscription, as a client does when a hop's heartbeats stop. The
origin server responds to each one as it responded to the first, and
with each response, the gateway gets three things:
- A heartbeat for the hop. When a response arrives, the gateway
knows that the origin server is still serving the push
subscription. It reads the origin status from the response's
Origin-Status header, as in the long-poll mode (Section 2.2), to
tell its clients whether they are caught up. If no response
comes within the interval the gateway picked, plus an allowance
for network delay, the gateway sends an origin status of "down"
to its clients, and subscribes again.
- Any changes that it missed. If the gateway missed a push, the
response holds the changes it missed, and otherwise the response
is a 304 (Not Modified).
- A new check of its clients' authorization. The gateway sends one
request for each set of credentials that its clients use, with
those credentials. If the origin server responds with a 403
(Forbidden), the gateway ends the subscriptions of the clients
that use those credentials. The gateway passes the 403 on to
each of those clients, with "Responds-To: container-request" in a
stream ([MULTIRESPONSE], Section 4).
If a gateway re-sends its requests more often than the origin server
wants, the origin server can respond to them with a 429 (Too Many
Requests) and a Retry-After header.
3.3. Either side can end a push subscription
A client ends its subscription at the gateway, as it would any
subscription. Once the gateway has no clients left for a resource,
it responds to the next push for that resource with a 410 (Gone), to
end the push subscription. The origin server ignores a 410 to a push
that it sent before the gateway's latest subscription request for
that push URL, since with that request, the gateway asked for the
pushes again.
The origin server ends a push subscription by responding to a re-sent
request without "Subscribe: webhook". If a gateway has gone away, a
push to it fails. The origin server retries a failed push a few
times, with backoff, and then ends the push subscription. And if a
gateway restarts, it responds to the pushes it doesn't know with a
410. So neither side needs a lease.
If pushes come faster than the gateway can serve them, it responds to
them with a 503 (Service Unavailable) or a 429 (Too Many Requests),
and a Retry-After header. The origin server then retries the push,
or replaces the changes it hasn't pushed yet with one snapshot.
3.4. Short streams can redirect to gateways
A gateway at its own URL needs the origin server to send clients to
it. The origin server does this with a handoff: it responds to a
subscription request with the catch-up, and then a 307 (Temporary
Redirect) sub-response that names the gateway. For example, the
county's site can hand each reader to a gateway at gateway.example:
Request:
GET /results
Host: votes.county.example
Subscribe: stream
Response:
HTTP/1.1 209 Multiresponse
Subscribe: stream
Content-Type: application/http-history
Cache-Control: no-store
Incremental: ?1
HTTP/1.1 200 OK
Version: ("2")
Content-Type: text/plain
Content-Length: 21
Bob leads, 51% to 49%
HTTP/1.1 307 Temporary Redirect
Responds-To: container-request
Location: https://gateway.example/votes.county.example/results
Vary: Subscribe
The 307 has "Responds-To: container-request", so it responds to the
reader's own subscription request ([MULTIRESPONSE], Section 4), and
says that the subscription continues at its Location. With its
"Vary: Subscribe" header, the origin server limits the redirect to
subscription requests, so plain GETs stay at the origin server.
After the 307, the origin server ends the multiresponse, and the
reader's browser resubscribes at the gateway, with Parents set to the
version it now holds:
GET /votes.county.example/results
Host: gateway.example
Subscribe: stream
Parents: ("2")
If the gateway has no push subscription for the resource yet, it asks
the origin server for one, as in Section 3.1. The gateway and the
origin server agree on the pattern of the gateway's URLs by
configuration. For clients that can't read a multiresponse, an
origin server can also hand off with a plain 307 response and no
catch-up, which fetch() [FETCH] follows on its own.
3.5. Verifying the subscription gateway
In the webhook mode, the requester names a URL for the origin server
to send requests to, so the origin server checks that URL before it
pushes to it:
- A push URL with the same origin [RFC6454] as the resource is
safe. A gateway on the path, such as a CDN, already serves that
origin, so it needs nothing more.
- For any other push URL, the origin server pushes only if the
subscription request is signed with HTTP Message Signatures
[RFC9421], with a signature that covers the @method and
@target-uri components and the Subscribe header, by a key that
the push URL's host publishes. With the signature, the requester
proves that it controls the host. A future revision of this
document will say how a host publishes its keys.
An origin server grants the webhook mode only to a gateway that it
trusts: its own gateway on the path, which it knows by its
configuration, or a requester whose signature it verifies, as above.
If any other client lists webhook in its Subscribe header, the origin
server ignores that mode, and grants another mode that the client
lists, or none. A gateway at its own URL serves subscriptions only
for the origin servers that have chosen it, by its configuration, so
that it doesn't act as an open proxy. A future revision may define
how an origin server publishes its list of gateways.
4. Implementation Status
This section records the known implementations of this document, as
[RFC7942] describes. It is to be removed before publishing as an
RFC.
No implementation yet supports the long-poll or the webhook mode.
5. Security Considerations
In the webhook mode, a client names a URL for the origin server to
send requests to. Without the checks of Section 3.5, anyone could
make an origin server send requests to any host, such as a server
inside the origin server's own network. So an origin server pushes
only within the resource's own origin, or to a host whose key signed
the subscription request.
A subscription gateway sees the content of the responses it serves,
private ones included, and holds the credentials that it forwards.
So its clients and the origin server trust it as they would any
proxy, and it follows the rules for credentials of a cache that
serves subscriptions ([SUBSCRIPTIONS-CACHING], Section 5.1). A
future revision may define a credential that an origin server gives a
gateway for one subscription only, to limit what a breach of the
gateway exposes.
If a gateway at its own URL served subscriptions for any origin
server, it would be an open proxy, so it serves only the origin
servers that chose it (Section 3.5).
Each re-sent subscription request costs the origin server a response,
so an origin server can limit how often it responds to them, with a
429 (Too Many Requests) (Section 3.2). Each push subscription also
costs it memory, and a request for each change. If anyone could
create push subscriptions, an attacker could create many of them, to
exhaust the origin server's memory, and to make it send a burst of
pushes at each change. So an origin server grants the webhook mode
only to gateways it trusts (Section 3.5), and ends a push
subscription when its re-sent requests stop (Section 3.2), or when
its pushes fail (Section 3.3). It can also limit how many push
subscriptions it holds for each gateway, and for each host of a push
URL. A push subscription that no client uses costs at most one push,
since the gateway responds to it with a 410 (Section 3.3).
6. IANA Considerations
This document has no IANA actions. It uses the Subscribe,
Heartbeats, and Origin-Status headers, which [SUBSCRIPTIONS]
registers.
7. References
7.1. Normative References
[MULTIRESPONSE]
Toomim, M., "Multiresponses for HTTP",
draft-toomim-httpbis-braid-multiresponse-00, work in
progress, November 2026.
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119, March 1997.
[RFC6454] Barth, A., "The Web Origin Concept", RFC 6454, December
2011.
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
2119 Key Words", BCP 14, RFC 8174, May 2017.
[RFC9110] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke,
Ed., "HTTP Semantics", STD 97, RFC 9110, June 2022.
[RFC9111] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke,
Ed., "HTTP Caching", STD 98, RFC 9111, June 2022.
[RFC9421] Backman, A., Ed., Richer, J., Ed., and M. Sporny, "HTTP
Message Signatures", RFC 9421, February 2024.
[SUBSCRIPTIONS]
Toomim, M., "Subscriptions for HTTP",
draft-toomim-httpbis-braid-subscriptions-00, work in
progress, November 2026.
[SUBSCRIPTIONS-CACHING]
Toomim, M., "Caching for HTTP Subscriptions",
draft-toomim-httpbis-braid-subscriptions-caching-00, work
in progress, November 2026.
[UPDATES] Toomim, M., "Replayable Updates for HTTP",
draft-toomim-httpbis-braid-updates-00, work in progress,
November 2026.
[VERSIONS] Toomim, M., "Resource Versioning for HTTP",
draft-toomim-httpbis-braid-versions-00, work in progress,
November 2026.
7.2. Informative References
[FETCH] WHATWG, "Fetch Standard",
.
[GRIP] Fanout, "Generic Realtime Intermediary Protocol",
.
[MERCURE] Dunglas, K., "The Mercure Protocol",
draft-dunglas-mercure-08, work in progress, August 2026.
[RFC7942] Sheffer, Y. and A. Farrel, "Improving Awareness of Running
Code: The Implementation Status Section", BCP 205, RFC
7942, July 2016.
[WEBSUB] Genestoux, J., Ed. and A. Parecki, Ed., "WebSub", W3C
Recommendation, January 2018,
.
Author's Address
Michael Toomim
Invisible College, Berkeley
2053 Berkeley Way
Berkeley, CA 94704
EMail: toomim@gmail.com
Web: https://invisible.college/@toomim