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