WebSub for subscribers and publishers: find the hubs of a topic, ask a hub for a subscription, answer the hub at the callback URL, and tell the hubs when a topic changes.
The code follows the W3C Recommendation of 2 June 2026 at https://www.w3.org/TR/websub/ and cites its sections. A subscriber is a server with a public URL, since the hub calls its callback. The hub itself is in dk.simongray.websub.hub.
WebSub for subscribers and publishers: find the hubs of a topic, ask a hub for a subscription, answer the hub at the callback URL, and tell the hubs when a topic changes. The code follows the W3C Recommendation of 2 June 2026 at https://www.w3.org/TR/websub/ and cites its sections. A subscriber is a server with a public URL, since the hub calls its callback. The hub itself is in dk.simongray.websub.hub.
(call! request)(call! request opts)Send the request, to subscribe or to publish, to its hub with opts,
and give the hub's answer as a map of :status and the final :url. In
ClojureScript, it gives a promise.
A hub answers 202 to a subscription request that it takes, and then calls the callback to check it. A status that isn't a success throws ::failed with the :status and the :message of the hub. An http hub is tried over https first, a request with a secret goes over https alone, and a 307 or 308 sends the request on to the hub it names.
The opts are the limits, :allow-pred and :user-agent of http/send!,
:send, a function that sends a request map in place of http/send!, and
the :prefer-https?, :https-timeout-seconds, :secret-over-http? and
:max-redirects of default-options.
Send the `request`, to subscribe or to publish, to its hub with `opts`, and give the hub's answer as a map of :status and the final :url. In ClojureScript, it gives a promise. A hub answers 202 to a subscription request that it takes, and then calls the callback to check it. A status that isn't a success throws ::failed with the :status and the :message of the hub. An http hub is tried over https first, a request with a secret goes over https alone, and a 307 or 308 sends the request on to the hub it names. The `opts` are the limits, :allow-pred and :user-agent of http/send!, :send, a function that sends a request map in place of http/send!, and the :prefer-https?, :https-timeout-seconds, :secret-over-http? and :max-redirects of default-options.
The default of each option of a subscriber or a publisher that has one, which follows WebSub:
The default of each option of a subscriber or a publisher that has one, which follows WebSub: - :url-as-topic?, false: without a rel=self, a document names no topic, since WebSub 5.1 subscribes to that URL alone. True makes the URL of the response the topic. - :body-links?, false: an HTML page names its links in its head, as WebSub 8.1 advises - :prefer-https?, true: an http hub is tried over https first, as WebSub 8.2 says, for :https-timeout-seconds at most, 5 - :secret-over-http?, false: a secret goes over https alone, as WebSub 5.1 says - :max-redirects, 5: how often a hub can send a request on to another - :signature-methods, signature-methods - :unsigned-status, 403: the answer to a delivery without the signature that its secret asks for, as WebSub 3.2.6 has it - :wrong-signature-status, 200: the answer to a delivery with a wrong signature, which WebSub 7.1.2 allows, and which tells a stranger nothing - :max-bytes, 32 MB: the largest delivery that a callback takes - :max-challenge, 512: the longest challenge that a callback gives back
(discover response)(discover response opts)The hubs and the topic that the response names, as a map of :hubs and
:topic, by opts, or nil when it names no hub. The :topic is nil when
it names a hub but no topic, e.g. an RSS feed without a rel=self, which
:url-as-topic? takes the URL of the response for.
The response is a map of :url, :headers and :body, as http/send! gives it. For the hubs and for the topic alike, the Link headers count first, and the document only when they name none. A document's links are those in the head of an HTML page, or the atom:links of an Atom or RSS feed itself, not of its entries. A relative URL counts from the URL of the response.
The opts are the :url-as-topic? and :body-links? of default-options,
and :links, the links of the document as maps of :rel and :href, e.g.
of a feed that you've parsed already, so that the body isn't read.
The hubs and the topic that the `response` names, as a map of :hubs and :topic, by `opts`, or nil when it names no hub. The :topic is nil when it names a hub but no topic, e.g. an RSS feed without a rel=self, which :url-as-topic? takes the URL of the response for. The response is a map of :url, :headers and :body, as http/send! gives it. For the hubs and for the topic alike, the Link headers count first, and the document only when they name none. A document's links are those in the head of an HTML page, or the atom:links of an Atom or RSS feed itself, not of its entries. A relative URL counts from the URL of the response. The `opts` are the :url-as-topic? and :body-links? of default-options, and :links, the links of the document as maps of :rel and :href, e.g. of a feed that you've parsed already, so that the body isn't read.
(discover! url)(discover! url opts)Fetch the topic at url with opts, and give its hubs and topic as
discover does, with the :status of the response. A request that fails
gives the :error as http/failure has it.
The opts are those of discover, and those of http/fetch!, e.g. :accept
for the representation whose rel=self is the topic, as WebSub 4.1 has
it, and :send. In ClojureScript, it gives a promise.
Fetch the topic at `url` with `opts`, and give its hubs and topic as discover does, with the :status of the response. A request that fails gives the :error as http/failure has it. The `opts` are those of discover, and those of http/fetch!, e.g. :accept for the representation whose rel=self is the topic, as WebSub 4.1 has it, and :send. In ClojureScript, it gives a promise.
(handler {:keys [wanted-pred deliver!] :as opts})A Ring handler for the callback URL of a subscriber, by opts:
A hub calls the callback in three ways:
The :topic and :hubs of a delivery are those of its Link header, which can differ from those of the subscription, so the callback URL tells the subscription, and the :request has it. In ClojureScript, a server gives the handler a body that it has read.
A Ring handler for the callback URL of a subscriber, by `opts`: - :wanted-pred, a function of the :mode, :topic, :lease-seconds and :request of a check, which says whether the subscriber asked - :verified!, a function of the same, called when the handler confirms a check that :wanted-pred said yes to, e.g. to keep the lease and the time to renew it - :deliver!, a function of the :body, :content-type, :topic, :hubs and :request of a delivery - :denied!, a function of the :topic, :reason and :request of a subscription that the hub denied or ended - :secret, a text or a function of the request that gives one - :subscribed-pred, a function of the request that says whether its subscription still exists, always true by default - the :signature-methods, :unsigned-status, :wrong-signature-status, :max-bytes and :max-challenge of default-options A hub calls the callback in three ways: - It checks that the subscriber asked for the :mode, :subscribe or :unsubscribe, of the :topic. When :wanted-pred says yes, the handler gives the hub's challenge back, which confirms it, and calls :verified!, and else it answers 404. The :lease-seconds is how long the hub keeps a subscription, and next-renewal tells when to renew it. - It delivers the topic, and :deliver! gets it as the bytes :body, if it has the signature that the :secret asks for. The handler answers 200 when the delivery came, so :deliver! should return fast. It answers 410 to a delivery whose subscription is gone, and 413 to one larger than :max-bytes. - It tells that it denied a subscription, or ended one, and :denied! gets the :topic and the :reason. The :topic and :hubs of a delivery are those of its Link header, which can differ from those of the subscription, so the callback URL tells the subscription, and the :request has it. In ClojureScript, a server gives the handler a body that it has read.
(link-header hubs self)The value of a Link header that names the hubs of a topic and its
self URL, the topic itself, e.g. for a publisher's answer to a request
for the topic.
The value of a Link header that names the `hubs` of a topic and its `self` URL, the topic itself, e.g. for a publisher's answer to a request for the topic.
(links hubs self)The links that name the hubs of a topic and its self URL, the topic
itself, as maps of :rel and :href. A publisher puts them in the link
elements of a page or the atom:links of a feed, e.g. as Hiccup.
The links that name the `hubs` of a topic and its `self` URL, the topic itself, as maps of :rel and :href. A publisher puts them in the link elements of a page or the atom:links of a feed, e.g. as Hiccup.
(next-renewal lease-seconds at)(next-renewal lease-seconds at seconds-before)When to ask the hub again for a subscription whose lease of
lease-seconds began at the instant at: seconds-before it ends, or
by default a tenth of the lease before, and a day before at most.
When to ask the hub again for a subscription whose lease of `lease-seconds` began at the instant `at`: `seconds-before` it ends, or by default a tenth of the lease before, and a day before at most.
(publish! hubs topic)(publish! hubs topic opts)Tell each of the hubs that the topic changed, with opts, and give
a map of each hub's :hub and its :status, or the :error as
http/failure has it.
The opts are those of call! and publish-request. In ClojureScript, it
gives a promise.
Tell each of the `hubs` that the `topic` changed, with `opts`, and give a map of each hub's :hub and its :status, or the :error as http/failure has it. The `opts` are those of call! and publish-request. In ClojureScript, it gives a promise.
The form of the request that tells a hub of a change, as most public hubs take it: the :fields that each request has, and the name of the field for the URL of the topic.
WebSub leaves this request to the hub and the publisher, and its note gives this form as an example. A hub that takes another can be given it as the :publish-form option, e.g. with hub.topic as the :url-field.
The form of the request that tells a hub of a change, as most public hubs take it: the :fields that each request has, and the name of the field for the URL of the topic. WebSub leaves this request to the hub and the publisher, and its note gives this form as an example. A hub that takes another can be given it as the :publish-form option, e.g. with hub.topic as the :url-field.
(publish-request hub topic)(publish-request hub
topic
{form :publish-form :or {form publish-form} :as opts})The request map that tells the hub that the topic changed, in the
form of the :publish-form of opts, publish-form by default.
The request map that tells the `hub` that the `topic` changed, in the form of the :publish-form of `opts`, publish-form by default.
(request mode hub topic callback)(request mode
hub
topic
callback
{:keys [lease-seconds params secret-over-http?] :as opts})The request map that asks the hub to subscribe the callback URL to
the topic, or to unsubscribe it, by mode and opts.
The mode is :subscribe or :unsubscribe. The callback should be a URL
that nobody can guess, one for each subscription and each renewal, e.g.
with a token in it. The opts are these:
A request with a secret goes to the hub over https, even when the hub is an http URL, unless :secret-over-http?.
The request map that asks the `hub` to subscribe the `callback` URL to the `topic`, or to unsubscribe it, by `mode` and `opts`. The `mode` is :subscribe or :unsubscribe. The callback should be a URL that nobody can guess, one for each subscription and each renewal, e.g. with a token in it. The `opts` are these: - :lease-seconds, how long the subscription should last, which the hub can change - :secret, a text of fewer than 200 bytes, e.g. a token, with which the hub signs what it sends. It can be a hidden text of wary-fetch, and the body of the request is then hidden too. - :params, more fields as pairs of a name and a value, for a hub that asks for them - :secret-over-http?, as default-options has it A request with a secret goes to the hub over https, even when the hub is an http URL, unless :secret-over-http?.
(signature method secret body)The signature of the bytes body with the text secret, by the hash
algorithm method of signature-methods, or nil for any other method.
It's the value of the header X-Hub-Signature: the HMAC of the body in hexadecimal, after the method and an equals sign, e.g. sha256=1f3a…. The secret can be a hidden text of wary-fetch.
The signature of the bytes `body` with the text `secret`, by the hash algorithm `method` of signature-methods, or nil for any other method. It's the value of the header X-Hub-Signature: the HMAC of the body in hexadecimal, after the method and an equals sign, e.g. sha256=1f3a…. The secret can be a hidden text of wary-fetch.
The hash algorithms of a signature that a subscriber takes, by their names in WebSub, unless its options give another set.
Most hubs sign with sha1, which is weak. A subscriber whose callback is an http URL should leave it out, as WebSub 8.3 says.
The hash algorithms of a signature that a subscriber takes, by their names in WebSub, unless its options give another set. Most hubs sign with sha1, which is weak. A subscriber whose callback is an http URL should leave it out, as WebSub 8.3 says.
(signed? secret header body)(signed? secret header body methods)Whether header, the value of X-Hub-Signature, is the signature of the
bytes body with the text secret, by one of methods,
signature-methods by default. The method is the one that the header
names.
Whether `header`, the value of X-Hub-Signature, is the signature of the bytes `body` with the text `secret`, by one of `methods`, signature-methods by default. The method is the one that the header names.
(token)(token n)A random text of n bytes, 32 by default, that nobody can guess, e.g.
for a callback URL or a secret. The bytes come from a secure source, and
the text is in the URL-safe letters of base64.
A random text of `n` bytes, 32 by default, that nobody can guess, e.g. for a callback URL or a secret. The bytes come from a secure source, and the text is in the URL-safe letters of base64.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |