A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 79KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  4. <!-- Has to be within the first 1024 bytes, hence before the `title` element
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Server-Sent Events: the alternative to WebSockets you should be using (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f7f7f7">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
  26. <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
  27. <!-- Documented, feel free to shoot an email. -->
  28. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  29. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  30. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  31. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  32. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  36. <script>
  37. function toggleTheme(themeName) {
  38. document.documentElement.classList.toggle(
  39. 'forced-dark',
  40. themeName === 'dark'
  41. )
  42. document.documentElement.classList.toggle(
  43. 'forced-light',
  44. themeName === 'light'
  45. )
  46. }
  47. const selectedTheme = localStorage.getItem('theme')
  48. if (selectedTheme !== 'undefined') {
  49. toggleTheme(selectedTheme)
  50. }
  51. </script>
  52. <meta name="robots" content="noindex, nofollow">
  53. <meta content="origin-when-cross-origin" name="referrer">
  54. <!-- Canonical URL for SEO purposes -->
  55. <link rel="canonical" href="https://germano.dev/sse-websockets/#comments">
  56. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  57. <article>
  58. <header>
  59. <h1>Server-Sent Events: the alternative to WebSockets you should be using</h1>
  60. </header>
  61. <nav>
  62. <p class="center">
  63. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  64. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  65. </svg> Accueil</a> •
  66. <a href="https://germano.dev/sse-websockets/#comments" title="Lien vers le contenu original">Source originale</a>
  67. </p>
  68. </nav>
  69. <hr>
  70. <p>When developing real-time web applications, WebSockets might be the first thing
  71. that come to your mind. However, Server Sent Events (SSE) are a simpler
  72. alternative that is often superior.</p>
  73. <h2 id="prologue">Prologue<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#prologue"><span class="icon icon-link"></span></a></h2>
  74. <p>Recently I have been curious about the best way to implement a
  75. <em>real-time web application</em>. That is, an application containing one ore more components
  76. which automatically update, in real-time, reacting to some external event.
  77. The most common example of such an application, would be a messaging service, where we
  78. want every message to be immediately broadcasted to everyone that is connected,
  79. without requiring any user interaction.</p>
  80. <p>After some research I stumbled upon an <a href="https://www.youtube.com/watch?v=n9mRjkQg3VE" target="_blank" rel="nofollow noopener noreferrer">amazing talk by Martin Chaov</a>, which compares Server Sent
  81. Events, WebSockets and Long Polling. The talk, which is also <a href="https://www.smashingmagazine.com/2018/02/sse-websockets-data-flow-http2/#comments-sse-websockets-data-flow-http2" target="_blank" rel="nofollow noopener noreferrer">available as a blog post</a>,
  82. is entertaining and very informative. I really recommend it.
  83. However, it is from 2018 and some small things have changed, so I decided to write this article.</p>
  84. <h2 id="websockets">WebSockets?<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#websockets"><span class="icon icon-link"></span></a></h2>
  85. <p><a href="https://tools.ietf.org/html/rfc6455" target="_blank" rel="nofollow noopener noreferrer">WebSockets</a> enable the creation of <strong>two-way</strong> <strong>low-latency</strong> communication
  86. channels between the browser and a server.</p>
  87. <p>This makes them ideal in certain scenarios, like multiplayer games, where the
  88. communication is <strong>two-way</strong>, in the sense that both the browser and server
  89. send messages on the channel <strong>all the time</strong>, and it is required that these messages
  90. be delivered with <strong>low latency</strong>.</p>
  91. <p>In a First-Person Shooter,
  92. the browser could be continuously streaming
  93. the player’s position, while simoultaneously receiving updates on the location of all
  94. the other players from the server. Moreover, we definitely want
  95. these messages to be delivered with as little overhead as possible, to avoid
  96. the game feeling sluggish.</p>
  97. <p>This is the opposite of the traditional <a href="https://en.wikipedia.org/wiki/Request%E2%80%93response" target="_blank" rel="nofollow noopener noreferrer">request-response model</a> of <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP" target="_blank" rel="nofollow noopener noreferrer">HTTP</a>, where
  98. the browser is always the one initiating the communication, and each message
  99. has a significant overhead, due to establishing <a href="https://en.wikipedia.org/wiki/Transmission_Control_Protocol" target="_blank" rel="nofollow noopener noreferrer">TCP connections</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" target="_blank" rel="nofollow noopener noreferrer">HTTP headers</a>.</p>
  100. <p>However, many applications do not have requirements this strict.
  101. Even among real-time applications, <strong>the data flow is usually asymmetric</strong>:
  102. the server sends the majority of the messages while the client mostly just listens and
  103. only once in a while sends some updates. For example, in a chat application
  104. an user may be connected to many rooms each with tens or hundreds of participants.
  105. Thus, the volume of messages received far exceeds the one of messages sent.</p>
  106. <h2 id="what-is-wrong-with-websockets">What is wrong with WebSockets<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#what-is-wrong-with-websockets"><span class="icon icon-link"></span></a></h2>
  107. <p>Two-way channels and low latency are extremely good features. Why bother looking
  108. further? </p>
  109. <p>WebSockets have one major drawback: <strong>they do not work on top of HTTP</strong>, at least
  110. not fully. They require their own TCP connection. They use HTTP only to establish
  111. the connection, but then upgrade it to a standalone TCP connection on top of
  112. which the WebSocket protocol can be used.</p>
  113. <p>This may not seem a big deal, however it means that <strong>WebSockets cannot benefit
  114. from any HTTP feature</strong>. That is:</p>
  115. <ul>
  116. <li>No support for compression</li>
  117. <li>No support for HTTP/2 multiplexing</li>
  118. <li>Potential issues with proxies</li>
  119. <li>No protection from Cross-Site Hijacking</li>
  120. </ul>
  121. <p>At least, this was the situation when the WebSocket protocol was first released.
  122. Nowadays, there are some complementary standards that try to improve upon this
  123. situation. Let’s take a closer look to the current situation.</p>
  124. <p><strong>Note</strong>: If you do not care about the details, feel free to skip the rest of
  125. this section and jump directly to <a href="#sse">Server-Sent Events</a> or the <a href="#code">demo</a>.</p>
  126. <h3 id="compression">Compression<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#compression"><span class="icon icon-link"></span></a></h3>
  127. <p>On standard connections,
  128. <a href="https://en.wikipedia.org/wiki/HTTP_compression" target="_blank" rel="nofollow noopener noreferrer">HTTP compression</a> is supported by every browser, and is super easy to enable
  129. server-side. Just flip a switch in your reverse-proxy of choice. With WebSockets
  130. the question is more complex, because there are no requests and responses, but
  131. one needs to compress the individual WebSocket frames.</p>
  132. <p><a href="https://tools.ietf.org/html/rfc7692" target="_blank" rel="nofollow noopener noreferrer">RFC 7692</a>, released on December 2015, tries to improve the situation by
  133. definining <em>“Compression Extensions for WebSocket”</em>. However, to the best of my knowledge,
  134. no popular reverse-proxy (e.g. nginx, caddy) implements this, making it impossible
  135. to have compression enabled transparently.</p>
  136. <p>This means that if you want compression, it has to be implemented directly in your backend.
  137. Luckily, I was able to find some libraries supporting RFC 7692. For example,
  138. the <a href="https://websockets.readthedocs.io/en/stable/extensions.html" target="_blank" rel="nofollow noopener noreferrer">websockets</a> and <a href="https://github.com/python-hyper/wsproto/" target="_blank" rel="nofollow noopener noreferrer">wsproto</a> Python libraries, and
  139. the <a href="https://github.com/websockets/ws" target="_blank" rel="nofollow noopener noreferrer">ws</a> library for nodejs.</p>
  140. <p>However, the latter suggests not to use the feature:</p>
  141. <blockquote>
  142. <p>The extension is disabled by default on the server and enabled by default on
  143. the client. It adds a significant overhead in terms of performance and memory
  144. consumption so we suggest to enable it only if it is really needed.</p>
  145. <p>Note that Node.js has a variety of issues with high-performance compression,
  146. where increased concurrency, especially on Linux, can lead to catastrophic
  147. memory fragmentation and slow performance.</p>
  148. </blockquote>
  149. <p>On the browsers side, <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/37#networking" target="_blank" rel="nofollow noopener noreferrer">Firefox supports WebSocket compression since version 37</a>.
  150. <a href="https://chromestatus.com/feature/6555138000945152" target="_blank" rel="nofollow noopener noreferrer">Chrome supports it as well</a>. However, apparently Safari and Edge do not.</p>
  151. <p>I did not take the time to verify what is the situation on the mobile landscape.</p>
  152. <h3 id="multiplexing">Multiplexing<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#multiplexing"><span class="icon icon-link"></span></a></h3>
  153. <p><a href="https://tools.ietf.org/html/rfc7540" target="_blank" rel="nofollow noopener noreferrer">HTTP/2</a> introduced support for multiplexing, meaning that multiple request/response
  154. pairs to the same host no longer require separate TCP connections. Instead, they all share
  155. the same TCP connection, each operating on its own independent <a href="https://tools.ietf.org/html/rfc7540#section-5" target="_blank" rel="nofollow noopener noreferrer">HTTP/2 stream</a>.</p>
  156. <p>This is, again, <a href="https://caniuse.com/http2" target="_blank" rel="nofollow noopener noreferrer">supported by every browser</a> and is very easy to
  157. transparently enable on most reverse-proxies.</p>
  158. <p>On the contrary, the WebSocket protocol has no support, by default, for multiplexing.
  159. Multiple WebSockets to the same host will each open their own separate TCP
  160. connection. If you want to have two separate WebSocket endpoints share their
  161. underlying connection you must add multiplexing in your application’s code.</p>
  162. <p><a href="https://tools.ietf.org/html/rfc8441" target="_blank" rel="nofollow noopener noreferrer">RFC 8441</a>, released on September 2018, tries to fix this limitation by
  163. adding support for <em>“Bootstrapping WebSockets with HTTP/2”</em>. It has been
  164. <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1434137" target="_blank" rel="nofollow noopener noreferrer">implemented in Firefox</a> <a href="https://chromestatus.com/feature/6251293127475200" target="_blank" rel="nofollow noopener noreferrer">and Chrome</a>. However, as far as I know, no major
  165. reverse-proxy implements it. Unfortunately, I could not find any implementation in
  166. Python or Javascript either.</p>
  167. <h3 id="proxies">Issues with proxies<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#proxies"><span class="icon icon-link"></span></a></h3>
  168. <p>HTTP proxies without explicit support for WebSockets can prevent unencrypted
  169. WebSocket connections to work. This is because the proxy will not be able to
  170. parse the WebSocket frames and close the connection.</p>
  171. <p>However, WebSocket connections happening over HTTPS should be unaffected by
  172. this problem, since the frames will be encrypted and the proxy should just
  173. forward everything without closing the connection.</p>
  174. <p>To learn more, see <a href="https://www.infoq.com/articles/Web-Sockets-Proxy-Servers/" target="_blank" rel="nofollow noopener noreferrer">“How HTML5 Web Sockets Interact With Proxy Servers”</a>
  175. by Peter Lubbers.</p>
  176. <h3 id="hijacking">Cross-Site WebSocket Hijacking<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#hijacking"><span class="icon icon-link"></span></a></h3>
  177. <p>WebSocket connections are not protected by the same-origin policy. This makes
  178. them vulnerable to Cross-Site WebSocket Hijacking.</p>
  179. <p>Therefore, <strong>WebSocket backends must check the correctness of the <code>Origin</code> header</strong>,
  180. if they use any kind of client-cached authentication, such as <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies" target="_blank" rel="nofollow noopener noreferrer">cookies</a> or
  181. <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication" target="_blank" rel="nofollow noopener noreferrer">HTTP authentication</a>.</p>
  182. <p>I will not go into the details here, but consider this short example. Assume
  183. a Bitcoin Exchange uses WebSockets to provide its trading service. When you
  184. log in, the Exchange might set a cookie to keep your session active
  185. for a given period of time. Now, all an attacker has to do to steal your precious
  186. Bitcoins is make you visit a site under her control, and simply open a WebSocket
  187. connection to the Exchange. The malicious connection is going to be automatically
  188. authenticated. That is, unless the Exchange checks the <code>Origin</code> header and blocks
  189. the connections coming from unauthorized domains.</p>
  190. <p>I encourage you to check out the great article about <a href="https://christian-schneider.net/CrossSiteWebSocketHijacking.html#main" target="_blank" rel="nofollow noopener noreferrer">Cross-Site WebSocket Hijacking</a> by
  191. Christian Schneider, to learn more.</p>
  192. <h2 id="sse">Server-Sent Events<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#sse"><span class="icon icon-link"></span></a></h2>
  193. <p>Now that we know a bit more about WebSockets, including their advantages and shortcomings,
  194. let us learn about Server-Sent Events and find out if they are a valid alternative.</p>
  195. <p><a href="https://html.spec.whatwg.org/#server-sent-events" target="_blank" rel="nofollow noopener noreferrer">Server-Sent Events</a> enable the server to send low-latency push
  196. events to the client, at any time.
  197. They use a very simple protocol that is <a href="https://html.spec.whatwg.org/#server-sent-events" target="_blank" rel="nofollow noopener noreferrer">part of the HTML Standard</a> and <a href="https://caniuse.com/eventsource" target="_blank" rel="nofollow noopener noreferrer">supported by every
  198. browser</a>.</p>
  199. <p>Unlike WebSockets, <strong>Server-sent Events flow only one way</strong>: from the
  200. server to the client. This makes them unsuitable for a very specific set of
  201. applications, that is, those that require a communication channel that is <strong>both
  202. two-way and low latency</strong>, like real-time games.
  203. However, this trade-off is also their major advantage
  204. over WebSockets, because being <em>one-way</em>, <strong>Server-Sent Events work seamlessly on top of HTTP,
  205. without requiring a custom protocol</strong>. This gives them automatic access to
  206. all of HTTP’s features, such as compression or HTTP/2 multiplexing, making them a very convenient choice
  207. for the majority of real-time applications, where the bulk of the data is sent from the server, and
  208. where a little overhead in requests, due to HTTP headers, is acceptable.</p>
  209. <p>The protocol is very simple. It uses the <code>text/event-stream</code> Content-Type and
  210. messages of the form:</p>
  211. <pre class="language-text"><code class="language-text">data: First message
  212. event: join
  213. data: Second message. It has two
  214. data: lines, a custom event type and an id.
  215. id: 5
  216. : comment. Can be used as keep-alive
  217. data: Third message. I do not have more data.
  218. data: Please retry later.
  219. retry: 10</code></pre>
  220. <p>Each event is separated by two empty lines (<code>\n</code>) and consists of various optional
  221. fields.</p>
  222. <p>The <code>data</code> field, which can be repeted to denote multiple lines in the message,
  223. is unsurprisingly used for the content of the event.</p>
  224. <p>The <code>event</code> field allows to specify custom event types, which as we will show
  225. in the next section, can be used to fire different event handlers on the client.</p>
  226. <p>The other two fields, <code>id</code> and <code>retry</code>, are used to configure the behaviour of the <em>automatic
  227. reconnection mechanism</em>. This is one of the most interesting features of Server-Sent
  228. Events. It ensures that
  229. <strong>when the connection is dropped or closed by the server, the client will
  230. automatically try to reconnect</strong>, without any user intervention.</p>
  231. <p>The <code>retry</code> field is used to specify the minimum amount of time, in seconds,
  232. to wait before trying to reconnect.
  233. It can also be sent by a server, immediately before closing the client’s connection,
  234. to reduce its load when too many clients are connected.</p>
  235. <p>The <code>id</code> field associates an identifier with the current event. When reconnecting
  236. the client will transmit to the server the last seen id, using the <code>Last-Event-ID</code> HTTP header.
  237. This allows the stream to be resumed from the correct point.</p>
  238. <p>Finally, the server can stop the automatic reconnection mechanism altogether
  239. by returning an <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204" target="_blank" rel="nofollow noopener noreferrer">HTTP 204 No Content</a> response.</p>
  240. <h2 id="code">Let’s write some code!<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#code"><span class="icon icon-link"></span></a></h2>
  241. <p>Let us now put into practice what we learned.
  242. In this section we will implement a simple service
  243. both with Server-Sent Events and WebSockets.
  244. This should enable us to compare the two technologies. We will find out how easy
  245. it is to get started with each one, and verify by hand the features discussed
  246. in the previous sections.</p>
  247. <p>We are going to use Python
  248. for the backend, Caddy as a reverse-proxy and of course a couple of lines of
  249. JavaScript for the frontend.</p>
  250. <p>To make our example as simple as possible, our backend is just going to consist of
  251. two endpoints, each streaming a unique sequence of random numbers. They are going to be reachable from
  252. <code>/sse1</code> and <code>/sse2</code> for Server-Sent Events, and from <code>/ws1</code> and <code>/ws2</code> for
  253. WebSockets. While our frontend is going to consist of a single <code>index.html</code>
  254. file, with some JavaScript which will let us start and stop WebSockets and
  255. Server-Sent Events connections.</p>
  256. <p><a href="https://github.com/tyrion/sse-websockets-demo" target="_blank" rel="nofollow noopener noreferrer">The code of this example is available on GitHub</a>.</p>
  257. <h3 id="reverse-proxy">The Reverse-Proxy<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#reverse-proxy"><span class="icon icon-link"></span></a></h3>
  258. <p>Using a reverse-proxy, such as Caddy or nginx, is very useful, even in a small
  259. example such as this one.
  260. It gives us very easy access to many features that our backend
  261. of choice may lack.</p>
  262. <p>More specifically, it allows us to easily serve static files and automatically
  263. compress HTTP responses; to provide support for HTTP/2, letting us benefit
  264. from multiplexing, even if our
  265. backend only supports HTTP/1; and finally to do load balancing.</p>
  266. <p>I chose Caddy because it automatically manages for us HTTPS certificates,
  267. letting us skip a very boring task, especially for a quick experiment.</p>
  268. <p>The basic configuration, which resides in a <code>Caddyfile</code> at the root of our
  269. project, looks something like this:</p>
  270. <pre class="language-text"><code class="language-text">localhost
  271. bind 127.0.0.1 ::1
  272. root ./static
  273. file_server browse
  274. encode zstd gzip</code></pre>
  275. <p>This instructs Caddy to listen on the local interface on ports 80 and 443,
  276. enabling support for HTTPS and generating a self-signed certificate. It also
  277. enables compression and serving static files from the <code>static</code> directory.</p>
  278. <p>As the last step we need to ask Caddy to proxy our backend services. Server-Sent
  279. Events is just regular HTTP, so nothing special here:</p>
  280. <pre class="language-text"><code class="language-text">reverse_proxy /sse1 127.0.1.1:6001
  281. reverse_proxy /sse2 127.0.1.1:6002</code></pre>
  282. <p>To proxy WebSockets our reverse-proxy needs to have explicit support for it.
  283. Luckily, Caddy can handle this without problems, even though the configuration
  284. is slighly more verbose:</p>
  285. <pre class="language-text"><code class="language-text">@websockets {
  286. header Connection *Upgrade*
  287. header Upgrade websocket
  288. }
  289. handle /ws1 {
  290. reverse_proxy @websockets 127.0.1.1:6001
  291. }
  292. handle /ws2 {
  293. reverse_proxy @websockets 127.0.1.1:6002
  294. }</code></pre>
  295. <p>Finally you should start Caddy with</p>
  296. <pre class="language-bash"><code class="language-bash">$ <span class="token function">sudo</span> caddy start</code></pre>
  297. <h3 id="frontend">The Frontend<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#frontend"><span class="icon icon-link"></span></a></h3>
  298. <p>Let us start with the frontend, by comparing the JavaScript APIs of WebSockets
  299. and Server-Sent Events.</p>
  300. <p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Websockets_API" target="_blank" rel="nofollow noopener noreferrer">WebSocket JavaScript API</a> is very simple to use.
  301. First, we need to create a new
  302. <code>WebSocket</code> object passing the URL of the server. Here <code>wss</code> indicates that
  303. the connection is to happen over HTTPS. As mentioned above it is really recommended
  304. to use HTTPS to avoid issues with proxies.</p>
  305. <p>Then, we should listen to some of the possible events (i.e. <code>open</code>,
  306. <code>message</code>, <code>close</code>, <code>error</code>), by either setting the <code>on$event</code> property or
  307. by using <code>addEventListener()</code>.</p>
  308. <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> ws <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WebSocket</span><span class="token punctuation">(</span><span class="token string">"wss://localhost/ws"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  309. ws<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onopen</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"WebSocket open"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  310. ws<span class="token punctuation">.</span><span class="token method function property-access">addEventListener</span><span class="token punctuation">(</span>
  311. <span class="token string">"message"</span><span class="token punctuation">,</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
  312. <p>The JavaScript API for Server-Sent Events is very similar. It requires us to
  313. create a new <code>EventSource</code> object passing the URL of the server, and then
  314. allows us to subscribe to the events in the same way as before.</p>
  315. <p>The main difference is that we can also subscribe to custom events.</p>
  316. <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> es <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">EventSource</span><span class="token punctuation">(</span><span class="token string">"https://localhost/sse"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  317. es<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onopen</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"EventSource open"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  318. es<span class="token punctuation">.</span><span class="token method function property-access">addEventListener</span><span class="token punctuation">(</span>
  319. <span class="token string">"message"</span><span class="token punctuation">,</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  320. es<span class="token punctuation">.</span><span class="token method function property-access">addEventListener</span><span class="token punctuation">(</span>
  321. <span class="token string">"join"</span><span class="token punctuation">,</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>e<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> joined</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre>
  322. <p>We can now use all this freshly aquired knowledge about JS APIs to build our
  323. actual frontend.</p>
  324. <p>To keep things as simple as possible, it is going to consist of only one <code>index.html</code>
  325. file, with a bunch of buttons that will let us start and stop our WebSockets
  326. and EventSources. Like so</p>
  327. <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>startWS(1)<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Start WS1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span>
  328. <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>closeWS(1)<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Close WS1<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span>
  329. <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>br</span><span class="token punctuation">&gt;</span></span>
  330. <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>startWS(2)<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Start WS2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span>
  331. <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>closeWS(2)<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Close WS2<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">&gt;</span></span></code></pre>
  332. <p>We want more than one WebSocket/EventSource so we can test if HTTP/2 multiplexing
  333. works and how many connections are open.</p>
  334. <p>Now let us implement the two functions needed by those buttons to work:</p>
  335. <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> wss <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
  336. <span class="token keyword">function</span> <span class="token function">startWS</span><span class="token punctuation">(</span><span class="token parameter">i</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  337. <span class="token keyword">if</span> <span class="token punctuation">(</span>wss<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">!==</span> <span class="token keyword nil">undefined</span><span class="token punctuation">)</span> <span class="token keyword">return</span><span class="token punctuation">;</span>
  338. <span class="token keyword">const</span> ws <span class="token operator">=</span> wss<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WebSocket</span><span class="token punctuation">(</span><span class="token string">"wss://localhost/ws"</span><span class="token operator">+</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span>
  339. ws<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onopen</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"WS open"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  340. ws<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onmessage</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  341. ws<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onclose</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token function">closeWS</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span>
  342. <span class="token punctuation">}</span>
  343. <span class="token keyword">function</span> <span class="token function">closeWS</span><span class="token punctuation">(</span><span class="token parameter">i</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  344. <span class="token keyword">if</span> <span class="token punctuation">(</span>wss<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">!==</span> <span class="token keyword nil">undefined</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  345. <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"Closing websocket"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  346. websockets<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token method function property-access">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  347. <span class="token keyword">delete</span> websockets<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">;</span>
  348. <span class="token punctuation">}</span>
  349. <span class="token punctuation">}</span></code></pre>
  350. <p>The frontend code for Server-Sent Events is almost identical. The only difference
  351. is the <code>onerror</code> event handler, which is there because in case of error a message
  352. is logged and the browser will attempt to reconnect.</p>
  353. <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> ess <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
  354. <span class="token keyword">function</span> <span class="token function">startES</span><span class="token punctuation">(</span><span class="token parameter">i</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  355. <span class="token keyword">if</span> <span class="token punctuation">(</span>ess<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">!==</span> <span class="token keyword nil">undefined</span><span class="token punctuation">)</span> <span class="token keyword">return</span><span class="token punctuation">;</span>
  356. <span class="token keyword">const</span> es <span class="token operator">=</span> ess<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">EventSource</span><span class="token punctuation">(</span><span class="token string">"https://localhost/sse"</span><span class="token operator">+</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span>
  357. es<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onopen</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"ES open"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  358. es<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onerror</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"ES error"</span><span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span>
  359. es<span class="token punctuation">.</span><span class="token method-variable function-variable method function property-access">onmessage</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token arrow operator">=&gt;</span> <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span><span class="token property-access">data</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  360. <span class="token punctuation">}</span>
  361. <span class="token keyword">function</span> <span class="token function">closeES</span><span class="token punctuation">(</span><span class="token parameter">i</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  362. <span class="token keyword">if</span> <span class="token punctuation">(</span>ess<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">!==</span> <span class="token keyword nil">undefined</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  363. <span class="token console class-name">console</span><span class="token punctuation">.</span><span class="token method function property-access">log</span><span class="token punctuation">(</span><span class="token string">"Closing EventSource"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  364. ess<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token method function property-access">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  365. <span class="token keyword">delete</span> ess<span class="token punctuation">[</span>i<span class="token punctuation">]</span>
  366. <span class="token punctuation">}</span>
  367. <span class="token punctuation">}</span></code></pre>
  368. <h3 id="backend">The Backend<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#backend"><span class="icon icon-link"></span></a></h3>
  369. <p>To write our backend, we are going to use <a href="https://www.starlette.io/" target="_blank" rel="nofollow noopener noreferrer">Starlette</a>, a simple async web framework
  370. for Python, and <a href="https://www.uvicorn.org/" target="_blank" rel="nofollow noopener noreferrer">Uvicorn</a> as the server.
  371. Moreover, to make things modular, we are going to separate the <em>data-generating process</em>,
  372. from the implementation of the endpoints.</p>
  373. <p>We want each of the two endpoints to generate an <em>unique</em> random sequence
  374. of numbers. To accomplish this we will use the stream id (i.e. <code>1</code> or <code>2</code>) as
  375. part of the <a href="https://en.wikipedia.org/wiki/Random_seed" target="_blank" rel="nofollow noopener noreferrer">random seed</a>.</p>
  376. <p>Ideally, we would also like our streams to be <em>resumable</em>. That is, a client
  377. should be able to resume the stream from the last message it received, in case
  378. the connection is dropped, instead or re-reading the whole sequence.
  379. To make this possible we will assign an ID to each message/event, and use it
  380. to initialize the random seed, together with the stream id, before each message
  381. is generated.
  382. In our case, the ID is just going to be a counter starting from <code>0</code>.</p>
  383. <p>With all that said, we are ready to write the <code>get_data</code> function which is
  384. responsible to generate our random numbers:</p>
  385. <pre class="language-python"><code class="language-python"><span class="token keyword">import</span> random
  386. <span class="token keyword">def</span> <span class="token function">get_data</span><span class="token punctuation">(</span>stream_id<span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">,</span> event_id<span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">&gt;</span> <span class="token builtin">int</span><span class="token punctuation">:</span>
  387. rnd <span class="token operator">=</span> random<span class="token punctuation">.</span>Random<span class="token punctuation">(</span><span class="token punctuation">)</span>
  388. rnd<span class="token punctuation">.</span>seed<span class="token punctuation">(</span>stream_id <span class="token operator">*</span> event_id<span class="token punctuation">)</span>
  389. <span class="token keyword">return</span> rnd<span class="token punctuation">.</span>randrange<span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span></code></pre>
  390. <p>Let’s now write the actual endpoints.</p>
  391. <p>Getting started with Starlette is very simple. We just need to initialize
  392. an <code>app</code> and then register some routes:</p>
  393. <pre class="language-python"><code class="language-python"><span class="token keyword">from</span> starlette<span class="token punctuation">.</span>applications <span class="token keyword">import</span> Starlette
  394. app <span class="token operator">=</span> Starlette<span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
  395. <p>To write a WebSocket service both our web server and framework of choice must
  396. have explicit support. Luckily Uvicorn and Starlette are up to the task,
  397. and writing a WebSocket endpoint is as convenient as writing a normal route.</p>
  398. <p>This all the code that we need:</p>
  399. <pre class="language-python"><code class="language-python"><span class="token keyword">from</span> websockets<span class="token punctuation">.</span>exceptions <span class="token keyword">import</span> WebSocketException
  400. <span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>websocket_route</span><span class="token punctuation">(</span><span class="token string">"/ws{id:int}"</span><span class="token punctuation">)</span>
  401. <span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">websocket_endpoint</span><span class="token punctuation">(</span>ws<span class="token punctuation">)</span><span class="token punctuation">:</span>
  402. <span class="token builtin">id</span> <span class="token operator">=</span> ws<span class="token punctuation">.</span>path_params<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span>
  403. <span class="token keyword">try</span><span class="token punctuation">:</span>
  404. <span class="token keyword">await</span> ws<span class="token punctuation">.</span>accept<span class="token punctuation">(</span><span class="token punctuation">)</span>
  405. <span class="token keyword">for</span> i <span class="token keyword">in</span> itertools<span class="token punctuation">.</span>count<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
  406. data <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token string">"id"</span><span class="token punctuation">:</span> i<span class="token punctuation">,</span> <span class="token string">"msg"</span><span class="token punctuation">:</span> get_data<span class="token punctuation">(</span><span class="token builtin">id</span><span class="token punctuation">,</span> i<span class="token punctuation">)</span><span class="token punctuation">}</span>
  407. <span class="token keyword">await</span> ws<span class="token punctuation">.</span>send_json<span class="token punctuation">(</span>data<span class="token punctuation">)</span>
  408. <span class="token keyword">await</span> asyncio<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span>
  409. <span class="token keyword">except</span> WebSocketException<span class="token punctuation">:</span>
  410. <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"client disconnected"</span><span class="token punctuation">)</span></code></pre>
  411. <p>The code above will make sure our <code>websocket_endpoint</code> function is called every time
  412. a browser requests a path starting with <code>/ws</code> and followed by a number (e.g. <code>/ws1</code>, <code>/ws2</code>).</p>
  413. <p>Then, for every matching request, it will wait for a WebSocket connection to be
  414. established and subsequently start an infinite loop sending random numbers,
  415. encoded as a JSON payload, every second.</p>
  416. <p>For Server-Sent Events the code is very similar, except that no special
  417. framework support is needed.
  418. In this case, we register a route matching URLs starting with <code>/sse</code> and ending
  419. with a number (e.g. <code>/sse1</code>, <code>/sse2</code>).
  420. However, this time our endpoint just sets the appropriate headers and returns a <code>StreamingResponse</code>:</p>
  421. <pre class="language-python"><code class="language-python"><span class="token keyword">from</span> starlette<span class="token punctuation">.</span>responses <span class="token keyword">import</span> StreamingResponse
  422. <span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>route</span><span class="token punctuation">(</span><span class="token string">"/sse{id:int}"</span><span class="token punctuation">)</span>
  423. <span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">sse_endpoint</span><span class="token punctuation">(</span>req<span class="token punctuation">)</span><span class="token punctuation">:</span>
  424. <span class="token keyword">return</span> StreamingResponse<span class="token punctuation">(</span>
  425. sse_generator<span class="token punctuation">(</span>req<span class="token punctuation">)</span><span class="token punctuation">,</span>
  426. headers<span class="token operator">=</span><span class="token punctuation">{</span>
  427. <span class="token string">"Content-type"</span><span class="token punctuation">:</span> <span class="token string">"text/event-stream"</span><span class="token punctuation">,</span>
  428. <span class="token string">"Cache-Control"</span><span class="token punctuation">:</span> <span class="token string">"no-cache"</span><span class="token punctuation">,</span>
  429. <span class="token string">"Connection"</span><span class="token punctuation">:</span> <span class="token string">"keep-alive"</span><span class="token punctuation">,</span>
  430. <span class="token punctuation">}</span><span class="token punctuation">,</span>
  431. <span class="token punctuation">)</span></code></pre>
  432. <p><code>StreamingResponse</code> is an utility class, provided by Starlette, which takes a generator and streams
  433. its output to the client, keeping the connection open.</p>
  434. <p>The code of <code>sse_generator</code> is shown below, and is almost identical to the WebSocket
  435. endpoint, except that messages are encoded according to the Server-Sent Events
  436. protocol:</p>
  437. <pre class="language-python"><code class="language-python"><span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">sse_generator</span><span class="token punctuation">(</span>req<span class="token punctuation">)</span><span class="token punctuation">:</span>
  438. <span class="token builtin">id</span> <span class="token operator">=</span> req<span class="token punctuation">.</span>path_params<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span>
  439. <span class="token keyword">for</span> i <span class="token keyword">in</span> itertools<span class="token punctuation">.</span>count<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
  440. data <span class="token operator">=</span> get_data<span class="token punctuation">(</span><span class="token builtin">id</span><span class="token punctuation">,</span> i<span class="token punctuation">)</span>
  441. data <span class="token operator">=</span> <span class="token string">b"id: %d\ndata: %d\n\n"</span> <span class="token operator">%</span> <span class="token punctuation">(</span>i<span class="token punctuation">,</span> data<span class="token punctuation">)</span>
  442. <span class="token keyword">yield</span> data
  443. <span class="token keyword">await</span> asyncio<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span></code></pre>
  444. <p>We are done!</p>
  445. <p>Finally, assuming we put all our code in a file named <code>server.py</code>, we can start
  446. our backend endpoints using Uvicorn, like so:</p>
  447. <pre class="language-text"><code class="language-text">$ uvicorn --host 127.0.1.1 --port 6001 server:app &amp;
  448. $ uvicorn --host 127.0.1.1 --port 6002 server:app &amp;</code></pre>
  449. <h2 id="bonus">Bonus: Cool SSE features<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#bonus"><span class="icon icon-link"></span></a></h2>
  450. <p>Ok, let us now conclude by showing how easy it is to implement all those nice features we bragged about earlier.</p>
  451. <p><strong>Compression</strong> can be enabled by changing just a few lines in our endpoint:</p>
  452. <pre class="language-diff"><code class="language-diff">@@ -32,10 +33,12 @@ async def websocket_endpoint(ws):
  453. <span class="token unchanged">
  454. async def sse_generator(req):
  455. id = req.path_params["id"]
  456. </span><span class="token inserted-sign inserted">+ stream = zlib.compressobj()
  457. </span><span class="token unchanged"> for i in itertools.count():
  458. data = get_data(id, i)
  459. data = b"id: %d\ndata: %d\n\n" % (i, data)
  460. </span><span class="token deleted-sign deleted">- yield data
  461. </span><span class="token inserted-sign inserted">+ yield stream.compress(data)
  462. + yield stream.flush(zlib.Z_SYNC_FLUSH)
  463. </span><span class="token unchanged"> await asyncio.sleep(1)
  464. </span>@@ -47,5 +50,6 @@ async def sse_endpoint(req):
  465. <span class="token unchanged"> "Content-type": "text/event-stream",
  466. "Cache-Control": "no-cache",
  467. "Connection": "keep-alive",
  468. </span><span class="token inserted-sign inserted">+ "Content-Encoding": "deflate",
  469. </span><span class="token unchanged"> },
  470. )</span></code></pre>
  471. <p>We can then verify that everything is working as expected by checking the DevTools:</p>
  472. <p><img class="g-image g-image--lazy g-image--loading" src="data:image/svg+xml,%3csvg%20fill='none'%20viewBox='0%200%20646%20288'%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%3e%3cdefs%3e%3cfilter%20id='__svg-blur-aa886b223fb216a8551fa2b5df639959'%3e%3cfeGaussianBlur%20in='SourceGraphic'%20stdDeviation='40'/%3e%3c/filter%3e%3c/defs%3e%3cimage%20x='0'%20y='0'%20filter='url(%23__svg-blur-aa886b223fb216a8551fa2b5df639959)'%20width='646'%20height='288'%20xlink:href='data:image/png%3bbase64%2ciVBORw0KGgoAAAANSUhEUgAAAEAAAAAdCAYAAAAaeWr3AAAACXBIWXMAAAsSAAALEgHS3X78AAALF0lEQVRYw7VYaVMiWxLl/3%2bazxMxETOv%2b6ntDooCKrYrrig2IIugIiCyyCKrgDnnZFUh0rTR2jNEXKrqLpl582aezLy2p0ZTavWGVGpPUn2qa9N3tEqt/qZ/uFn9uUIR65vSaLaUDvve0jOepXJFCqVHaXWe5anxOm48n/Q5jhf7SC9fLMljpSrNducN/dF3PrmnOuSpmHuw6LCPY9UhPrbM/YMUIRwHytWadtabzTfCv6eAQqksqXRGkqm00qlhTa5QMhgNzaMCio9luYzG5A48Gy1DwIYKWn%2bXT61eVwVUIF88ca30rQ2y39p4qVxWJWVzOblOZfRguNZQXFtu7zKSfcgP5nO97d8TMzLrcIrT5ZHZxWWxr7hlctYukcStCll%2bxwLYSPzs/Ey%2bTC%2bKd3tX5hbtsuT2yt7RidiXVyUBQbhJnj7neze/y8bOnrjXNmR1fVO%2bTM1J9B1elgIfq08SjUZl9%2bBY9o9OQXtFlt3rMudYlXn7srhAb2phWb7v%2bsTudMk6%2bDg967L2fVc2t3dUpr%2bnZmXObqxbda/JWSgmtnDsSrb3fLKxtSOB4KWc/wjifU/SufyrqY5plukWcKo3Nzfi3fGJD5te/74jJ%2bcXKsgmNnp1c6ebo6UUHyuSvc/J0empOKCc9e19zNmX9P2veQ0sDXxyDw/iD1yI79gve75DCV5GZe/wVOUPhiNy6A/Ij8sY9hGW/cNjjEdwEKdycHwiO75j2cWao9OA9i86HOI7C4qNpkS/qrfaalb6jffhUy5XX310uHHOA8yRrmP5V73ZHvhgfQgX6AJ5WAHHaA162hhrgXdliNfb9ooBDzB1rrEwqzEkr8WbfRwz%2bBKX2qr8GsZ0j%2bjnHLpDLl%2bE9dbEViwWJQ8gGzyhafpVAa0IQflN3yuYfRReAc38Tmfudd4j%2bqzG74eCQY%2bNa%2b7hexmcfgn0ynAbi5aOkxf63/Dgt44b31ybAw3S5rc%2bMTc/xMfgafWNG3t9L5n8bDe5hmyHW%2bJPtKTVeNINEcgaAI8aNF4sFOQyEpUnat3soyk%2b4dQaMNtHAE8OoJNIXstt6k6S19fKoAUgrWM%2bG%2bdVKhWpQpGPpaLcpbPSxilUq1WpY6zRgI9DoKa5plarQdCCzufaJlqJ1gO5OMZ1%2bXxeFWmtaZi8fqcNz7WFUk35h6sj80dt6XcbsrzskuVVj3g2d2R3b0%2b%2bARAnJqbEubYpSwAXJ8YWMcdSCK0jeRWXhaVVmZlbkInJb3J0HpZOp2Mw0Q02VOgylOVaxTy7U4HKC9yhX66teWR63gHabplZBCCvugBsDskVy6pIVSDWZ9JpWcGcNa8XoOfVNcnb1IBXw%2bT1kWaL3jXkn96WrPib8txpSDAYkkOAxvlFEEDjg4BHEovF5EcoLAcAlONTvxz7z3XjqgCcAsfPAkEJXPzQ8Rsgfwu%2bxxN7tYCqWksgEJCTswBonUgAgMvn6dm5xOJX4seTCM/3c8wjvjSbDaVB18o/5GRyelZ29g8g3w9VYi5fUCuweH202VoKCl2A0TOEbkq32zXbs7y8vIi89PW71%2btKv9/T1uv1dIM0Y/rS8/Oz9r9gLtfwm0JxjjWPbsVT5Nouxnug2cd7z%2bTHNXySH9/ZuI50%2bCzDhSxlWjR0Hk5/mNdHm41/2WwWG3mU0R9Nq9czNjXarB9jvCVwC4K2gba9fv8nWtb4KP3RH9cPfkN8muDTxcaN7mFZ5I9%2bNqIiAYbgR%2b1fBIOys3eg%2bcD%2b/qEUK09jmL62DlJbgiITj43NLfEdHMhtNq9r%2bv3%2bQFmWVVxcXMjRiV82kC8sOpbFh/ejkzO4zpmcwe1IYwdx3eneQPz2W5qAog3rfE%2bWzzQbQ0Emew8ETyvxEyQpaxtb8NMLicYSGj8tpsO/l6GTYSyPRCJyAmxIZ5gSV39aQ0XRbI8OD2V9a1e2kCjt7RnJ0ya%2bmdjsHgAP/GeqBCZjISQ6byyg25P/9c9GrdI3eTrDAlvvfFonOdysPkYDuomMqMca75vu0FFfbX1IOK613KmBZOb5uftLeT5tAQSnByQYj8AAKqEDP7XAhbH7vRDC%2bXQfvrdN1CemNM3QNTyPMZ1ARtpURtd0Cb5b7fm5Y9CCDARhPvlNcGaeQFlHaf9ps6Xu0prExOIJoOyTXCUSCHmXMP84zNEvMRQqnU5bE59xYYTh7eb2VraQ9%2b/CpPeBAUmEQSryaSgRYh5QLJXk6upKIgibDLMeFDAB8IqgQgwjP49fJWR7d18ugD%2bHwIZThMuWKqGuiq7WjMToM%2bHul2GQCkhnsigzkxC6JW6PR75Ozmh15XC6JQVwbKN/NNGwvsuMz8javm9vo8LzIn6fy9VtGhbUNpkYmRwVVYMSnE6nVptTMwsyt2DXpGt2wQFeq6hEPaCxLpPfZuQvVIk7AOGWaQWMUjVTAaMy/JEFlHAqyesbVQI7aMZsTFroGjw5nsDYlBLziwBRY03NENJcPzqP7kRl1c10ularqlmTDzNEvltzanAX472scxtmKkw34vqnIR7Wu9E%2bYQEElDZ9UpOd/lgwYuIxrnEtNzwu7o/O40kyLf7sj%2b7ESDIKkr8r66%2bajZlc8uZWbu%2bMMMgTZ5pbgzbz%2bQdJXKfeFYyJCwucwI%2bQ1uRnqNfvUWr%2bFAZN8CN9prVUhlFxlhCCU3pXwFNkgcMD4emXzfS5jgjAUMsNx%2bNxpX%2bFdJmhs4ySOYUirPj4qPcHn0iECnqKmWxOw9f6xib80SX/%2bmtS9pBzXwQj0nsxsrJxYYSh6eY6ISueTbEvOWV6dl4uIomfEiEqgIXN/PyiOD0bKJzmZWIWBZBrXRYWFrX4mgcmOFxesaPYcrs8eokxBTzwnV4oDVads7Nz8nV6Hs2OnOJAvs3b5evEtCw4lsS9uf%2bG72%2bFQfoNy9j73IMuZqjiqbDeZhii2b2XbdK0eVr0UeYTxAFeqPycCneVnoEJTfXXCkCNFlStGHcIRaVR08yScziXVtLRuqCjIZSYQ8tg/kGL4h0BrYSW3PhgnjFIhCwMIMGB9piEMPfWdLcztlEggtYodrAwMpT3Oo8mT0AjbRZWFjb0zPx%2bYGHo62vrmbL0tWiiYpgD6FT6/sC9Xgbre%2bYePtJs9/c5CUeiiP9J44IDJ/hohhxiwGU0rsL/CkWp/Ww2ozRiiONhpMS8aW4NlagaLgcIXx1UduTDKJRBBGJNYkUBSw6etiZAkKVoJlzJZFKvx1K3N%2bI7PMbJV%2bQexRwtRS9YPpMHFIADyetbCF2Xde%2bmrLjXZBJxmrmAFzk5QagxLuaij8nJVTymFyL0/78npuToPPTmkoI1vWaC2NSi3SHLrjX4%2b6JMzS8p3kzDrz0ojpaWnbKEImgFuOBadaFeOJC5%2bQXZPfQLy/Y7gOW3b9PAjkX5z98z4nG79Rb7y9cp0HGK27trWNsH8gNbBtoLIQuLAlV5aqHwpfjPA1qd8YIjFIlpJVb/xYUDTy2B7JERgFdnzOLukDwxfR29EKECeOESRdYZCoWRDYbk%2biYll%2bEwwDaMzPPcuN09PkXGmESGmtQLlmsUalR0IZ%2bXAKIMb4ZDkThA8R6Z5xHkDGr0icaTby5ifssCLGCyLh7ob/Q7MYuO4cuP4cYTYYpMF7AuRPqmXzPPt9a0TCwg6NEK6LPG5UpffZnYoz6vfF%2bUjvozLz26ZvGDd7qGVbBZuEC%2bFiYYmNUdK%2bt7zTaKiu9dfoz7Ea1/Cjtj5jEKWBciH6nW%2bn2DWrP1Wg4Py/URWcdGgT8pJfkjQFoZ2XvzGE5brfanLjSMm6f/z4XIfwERJGHCHQ2p7wAAAABJRU5ErkJggg=='%20/%3e%3c/svg%3e" alt="SSE Compression" data-srcset="https://d33wubrfki0l68.cloudfront.net/d9aa3cb7e8ec5b4cc500b1e2b4c9275c8f73bb7b/3df27/assets/static/sse-compression.0a1763c.a6a3649bb9d1351f6e26788e472622a9.png 646w" data-sizes="(max-width: 646px) 100vw, 646px" data-src="https://d33wubrfki0l68.cloudfront.net/d9aa3cb7e8ec5b4cc500b1e2b4c9275c8f73bb7b/3df27/assets/static/sse-compression.0a1763c.a6a3649bb9d1351f6e26788e472622a9.png"><noscript><img class="g-image g-image--lazy g-image--loaded" src="https://d33wubrfki0l68.cloudfront.net/d9aa3cb7e8ec5b4cc500b1e2b4c9275c8f73bb7b/3df27/assets/static/sse-compression.0a1763c.a6a3649bb9d1351f6e26788e472622a9.png" alt="SSE Compression"></noscript></p>
  473. <p><strong>Multiplexing</strong> is enabled by default since Caddy supports HTTP/2. We can confirm
  474. that the same connection is being used for all our SSE requests using the
  475. DevTools again:</p>
  476. <p><img class="g-image g-image--lazy g-image--loading" src="data:image/svg+xml,%3csvg%20fill='none'%20viewBox='0%200%20620%20270'%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%3e%3cdefs%3e%3cfilter%20id='__svg-blur-0a52f18c0d2930b06db6e22b1898fd5c'%3e%3cfeGaussianBlur%20in='SourceGraphic'%20stdDeviation='40'/%3e%3c/filter%3e%3c/defs%3e%3cimage%20x='0'%20y='0'%20filter='url(%23__svg-blur-0a52f18c0d2930b06db6e22b1898fd5c)'%20width='620'%20height='270'%20xlink:href='data:image/png%3bbase64%2ciVBORw0KGgoAAAANSUhEUgAAAEAAAAAcCAYAAADRJblSAAAACXBIWXMAAAsSAAALEgHS3X78AAAMIklEQVRYw61Y%2bW9kxRGevyBkgQ2wC4mU5MdIIVESCIQIBSQkEoGEkKJIiQIhSgSbXfaAECmHsoBAJBG5CBKEcOwuu2vjY%2bzxMb6P8bUeH%2btr7PHMeDwznvO9ua83l79U9XvPHo8PdgFLn7tfd3VVdXdVddUY3N4NhCIyAuEIAqEI/KHwjjYYlg5EKCLB4wsI2pC0zUeHn9uwCqZjWWGmC0WuS041PD6/aJmPv1qO1up9pmG5uoyA0Ffe2i/PG3gDmVyekENOKaBQKkEpFqktI18oIJXJELL7Ip3NQYrGEEskESUwr3yhiGxeEVCKJWrzSKYzRBeHHIsjQvRMw/xZDss7SEYteH0ynUZYjgr%2bCvESLetNbaFKJuuj7ylDurKeUiyBLI3z3gxSNIrWtg509AxgfGoWQ5Yx9A2PYWR8Atari7TBPBLJFBKp9L6I0/z8wjwaTGaMTlhhnZ5GZ08/FuxOjF%2bZhG3VJRSPxpOwLS2iqb0bizY7LONWIYflpjK5A2Vsy0ojFo8TXyvGrbOYmr2KEZI5PDqBURobnZgU/Mat0%2bjq7oN1dp5kXIFlYgpO1xpaOswwdfZgPRCmA8nCEJQkjBGDxtYOvHPuEuqa2tFnGce5CxfROTCGTL5Ap5YSgvcEKRWJJuB2u3G5yYTWjm68/2EdCepBq7kXH9Y1otcygSRtMCzHEAyFhMIXPzLig4uNeJ/kGDv7kcoqB8vREEukhRVN0ib7hsbQbGpHe98Qmlo60NU7gPP1jWg0daF7cASmdjPa6WIv1jehq9%2bCJbqQ9u5emMx98AYlcSkGWZZQKRewWVZQKuZEv1JSRFsq5qEoWYGCBqUa%2bQyKBbq5RAzZTGqLpkjreFyfZ%2bSpH49HUaB%2bJkVuIIURjUpbcnbxPgDxeEzQZzNJanMok761YNkFTTbz5zHu87p8LiP2yjSGqWU/rKsyxm0RTCxLmLKrmF6VMONQMUuw0ve01p91bM9NO2RMrQSIPkKQBQ2vVdfLW30G06nrZMytxQS2ZDg1aP0t/qu7wTqznBmnLGisrPPqTkzXyq7m4dj%2bNjzwqgdfOBnEt14M45tnw7j9TBBHzwRw84kAPv8bP27S2i8%2bHyQ6tc84dNwvvm96NojvvujFV17w45ZTAdxyUsWtp3a2R08HcO9LXtz8rPp943GVD7cs43PH/LiBvm84poLnmJbX1%2bK%2bl1U%2brCO3rO%2bR03vTfhwM97/iwdf/LOPX5%2bKEBPUjuO%2bvMh79ZxQP/E3Gva9IeOgfUTx9Po6f/y%2bG%2b16T8dDrMh55I4qHqT10IkQH4MM36PCefDeOp96L4%2bkLCTz%2bnygJCOLImRBuI9zxvEr3NeLPsn5E/O//i4zvE79vvxTBY2/G8Mi/ovghjT9MePTfUTxGPG45FaINhgQfxtHnQrjrrBc/eD1K%2bsTxk7dieILkPkg633giKOaPnLl2iAO486yEPxmT%2bGNzEneSgve8JuH5ugROEX72dgx/MKZw7IK6uWcuJnCc%2br9tSOLxN2SygBDuPuvD916L4AlS6Kn34zhxKYEfvxnFbae3FfoSWdB3iO7Bv8v4fWNS8H6GDvWn78Rwinj%2bitb9kg7mZVMKv6A%2bfz/1bowOIIjbn1MPgcF9triH6bCO0foniYYP%2b6u/C%2bFWlncmdF0QB3D4ZBj3vCrj7ldkIeCmE3xzQRwm87rjuaDYCN/mzWTurBB/M26nuUPHQ2JjX34hINYwHbsCK8Pucfjk9rq7iI7dTadjFzp6RuXDbnAj4TCNsVzuHzm9vf5wFS92pUPHgypObOvG84efvT4YLLYYBu1FdM6lCGnqFzC8WhDtiLOEYUeJ2jIGVxQM2RXR6hgQY0RvS2CY5kZcFViIflRrmb4aluUk8S6qa6hlvowh6jO9xaGP6f2K6O/io8kbtm/rVEtzrTBU6DlAOYe4FBBAKQugQiihUqBnLBtDVAriwL8KPaNEKwW9CPrWqPXQUxfbRbZZytP/AqFMHwoysSACG17RJ0FiLpeUIYd9Qn4qGoCSS%2b0Wxzp/Rn8GzvTCUlQkBQmRaKSwurKC985fRnNHL65MzeDK9JIgLpUrKFc2t1Aqb4IaSjcVJFMZdPcPwkQJ0PjEBGZtTrGmWFLXFDnlLZSwbFuihOsyzl1qRLPRhPqWTjQ0m9DRO4i6hhZKbkZF4nKR%2bpfqG2BzelU%2bVbKT6ewOPT4NDJwf5ylnX/f64CHkFU4YFPg2AghHIuBUmecP%2bstSHVGuVJDNZuAPBKmluqJmzebmpuCdz2Wx4aeiiAukYAg%2b6vMaj29DfPsDIZIrCV1YPufutX%2bs82dmAbF4grVDuVymrKws2hLdlq60/lekYqJERUU1eIzpkynKAqkgqd1w9RruJ6lmYPpa3nq/eqxqcpdsrk2KxdIufT4JDBKVlHEqLhxOF2EN0VhMswKq1KjltJNvKUuFQ4YqsAxVUDrSBL71KFeDtC5CNxYMBul2w5BpjNektTXcSlS95chaisWCEJ5KJkQ5zRvMkWUUSGYykaA0WSL3KiFNB5vgCrNKJkOS5V1jnxSGGBUWbJJsbiGqm4OEVfsK2ju7YKbiYtgySn45IkrXjNhwdguZjNrGSUkuctrMXTBSUTUwMIArswuUbxfFxtWDyNDNJbFM1aCpsxstpg4YjS2oN7bDTAXKAMnp5HbQQjGhBe1dPbhc/xHmbA5xGdWyZTrIaj0%2bDQx8e6y8a80tysUIWYTP4xZKWqdn0UuBbW5xheJAfs8TZCbMg817hQ5unMpRl8sF/qGF16R1a6GDYNN1r7nQRrz76VAnqFTmMnZw2EIHMIb%2bwWFMUBlrGR1Hd9%2bgKKVXXe4dfFQLiH52FsAuwAJ4E2z%2bvCFFUUQ8UGic5/TTYvOtBo%2bJtbSOrYFNmd1GEcjvWKMfFAdUEWvIBfIaf0VzN9UVcqJlMB/V0jI7eEU199LBPGp1u1YYklTPU6QRN5iifrlc2TNacoDaC%2bIVoKjMypY4kG5WCJs7gh2jQq9EOpP9VBGbeTAvdqfqP132fjoeBAMrz4EmGFb9nw8kmYjD5V6HV3sap2bmhJ/vF705aHJAGxe/yFgxNDyMuSX7DhpWPq8UEIvKsNkdQt7KqkME3hXKO2bmbdjY8MO97oXX56dbjtK8i%2bKTH3aHE4lUZosP/%2bTlca/BOnMVV%2bcXhds4133a/Ob1PYP8hkcpEPIhcEBM0emurzlhbOtEEwUjIyU2VxdWxNxelsDWwwr5N3zoGxwRMaPV1Ibx6fkd9GXtiZ2ZtqKuuQ09ff0i4ens6qcYMIweSoDMZjNaOnvR0tomflIbGrVicnISF%2boa4fGHBT/mwe4xNzuD1o4uSr6GMDkzL377O8hS97UANktmykkIWwA/T%2byj7NM5zT9109vvj5Me9idew9bA39zWulCOlGRZBdoAf/OzJ%2bSJJCorEiqm4XnVXdQ1rEe1/NpEiHX8xImQLMeEEuxHhYIilOKkRk2ISlsKlkpFNWBtBTlFfDM9J1OKoiuhJTVs8lX03Gc6pucAyLKqExLeILf6DW8nWuoY0%2bv82BqFjtpakchpQbVav2uBgd9UiRKPZfsqPWOrIglJUQLCkTZBSUmC4gH7qIdSY/Ec1SRDWQIrFA4FMUf%2buGhbxvzCIvmtawc9t5wcMW9OZFL0zX7OcsL0DLs9PuGGPMatTideJxrjIJ3V%2bMRiCSGPn273ugdX5%2bbhWFsXG6rV79oSIQo%2bMgnhzXNCtLQwj8sNRtRRInKhrgkd5h4sO9wig6tNhhj8vnvW3WiiJKi%2b0YgPzp2HeWBU/Pae1ehVxbIYG7Xgvx9cQluHGW%2b9dx51HxnR3NKCBlMnWlpaUU/xob6hGV19A8TPjMGhIbxNdEt2FyVWBfFScU4wPDhABVU96ptaRbxwUfAsFvbW78BESFgAnTRHWoY4iHAI84s2LC7ZsEop8gL1I5zGam9yLeJkKV6vl6zIIZIptqQ1UihXYwFxcoENCpYc%2bdfolbHTK8Ctz%2bcTspeXl7G0bCeLs2NuYUkUSU6nk6xqRfwUrmeUbEmSFKHXYkNksR5KuvbT7WMtgJmxf2e0E9HjAAdC9v2yiAFlMbaXD/F4XIsBejHF9GUtrlTHAKbTfb68VXiVdozxWparxx19nPXSeXEsEfGhau31%2br6O/wODFRuSjruYwgAAAABJRU5ErkJggg=='%20/%3e%3c/svg%3e" alt="SSE Multiplexing" data-srcset="https://d33wubrfki0l68.cloudfront.net/a214dba7e635b9c1705b811e1f9e4f1ec063ff1d/35a94/assets/static/sse.41b9fa4.1efd0be9ccdca2dac72468578f5a26a7.png 620w" data-sizes="(max-width: 620px) 100vw, 620px" data-src="https://d33wubrfki0l68.cloudfront.net/a214dba7e635b9c1705b811e1f9e4f1ec063ff1d/35a94/assets/static/sse.41b9fa4.1efd0be9ccdca2dac72468578f5a26a7.png"><noscript><img class="g-image g-image--lazy g-image--loaded" src="https://d33wubrfki0l68.cloudfront.net/a214dba7e635b9c1705b811e1f9e4f1ec063ff1d/35a94/assets/static/sse.41b9fa4.1efd0be9ccdca2dac72468578f5a26a7.png" alt="SSE Multiplexing"></noscript></p>
  477. <p><strong>Automatic reconnection</strong> on unexpected connection errors is as simple as
  478. reading the <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html#last-event-id" target="_blank" rel="nofollow noopener noreferrer"><code>Last-Event-ID</code></a> header in our backend code:</p>
  479. <pre class="language-diff"><code class="language-diff"><span class="token deleted-arrow deleted">&lt; for i in itertools.count():
  480. </span><span class="token coord">---</span>
  481. <span class="token inserted-arrow inserted">&gt; start = int(req.headers.get("last-event-id", 0))
  482. &gt; for i in itertools.count(start):</span></code></pre>
  483. <p><em>Nothing has to be changed in the front-end code.</em></p>
  484. <p>We can test that it is working by starting the connection to one of the SSE
  485. endpoints and then killing uvicorn. The connection will drop, but the browser
  486. will automatically try to reconnect. Thus, if we re-start
  487. the server, we will see the stream resume from where it left off!</p>
  488. <p>Notice how the stream resumes from the message <code>243</code>. Feels like magic 🔥</p>
  489. <p><img class="g-image g-image--lazy g-image--loading" src="data:image/svg+xml,%3csvg%20fill='none'%20viewBox='0%200%20836%20728'%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%3e%3cdefs%3e%3cfilter%20id='__svg-blur-56b8ef60bc5a52f2598b1e8ef1b1a2dc'%3e%3cfeGaussianBlur%20in='SourceGraphic'%20stdDeviation='40'/%3e%3c/filter%3e%3c/defs%3e%3cimage%20x='0'%20y='0'%20filter='url(%23__svg-blur-56b8ef60bc5a52f2598b1e8ef1b1a2dc)'%20width='836'%20height='728'%20xlink:href='data:image/gif%3bbase64%2ciVBORw0KGgoAAAANSUhEUgAAAEAAAAA4CAYAAABNGP5yAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAUoklEQVRo3t1aB3uVVbbOf7iKOqSdnpyck3PSQ8pJ7703IIUUQiAYpINebKhzda4z984zjlewjQURpINUIbRQI4QSkN57r4rvfdc%2bOSGgMwNzQeM9z7Oevb/97br2Wu9615e4nTx5EqdOncLZs2dZnoY8nzx56jHISTV/Z2cnTpw4gQsXLuDcuXM4f/78rypu4O/mzZs4cuQofvjhBzzunxz89u3b6C0/pYCLFy9h%2b/YOXLt2TTXeuXMHP/744yMVmVN%2bZ2hpN67f%2bMlGRPnff//9T9vZJu2Pej8ucXNVXL%2be9Uf167mGuJpYwJXLl3H16jVcv3YVFy5e7O4rz%2bvb2rCjYxs69%2b77yTyPeo9uPSd9HAvcP7fgjfzefvMNTJsxGwu%2bmob//O93ce3KJXyzshUH9u1FVU0NJk0ci6l/m4arly9gxsxZuPPj47kgt54b/CUUIGAov/966w1EREQi1hGNya%2b/iYa6GgQEheDlVyfj3ydNwptvTMb/TH0fo0Y%2bh9LSUvzpz39V41zu8EgV8Lh/P6eASRPG4rXf/wF/%2bdNbGN4yEqlJiSgsqcCHH36I/v37Y9zokRjWPByNQ5rwbHMzxk2c9P9DAafPnFHl5o0bsHffARzavxft23dg66Y2/P4/3sTxEycxf95crFixAouXLMP6Nasw6cWXce7CJSdAPw4X%2bCUVcPbsOVy/fv1fnueRg%2bAvr4Czinf8yLAooa%2bnSHSQcClm7gyL97Y/aGi7f81eqYDHvV6vt4CrV6%2bTil4iM7xESkxafP5i1/NlErMrlMvq%2bfLlK7hx46bqI%2b3n2e/s2QvOMZQLF%2b/tf%2bnSld5tAcL/r127jgMHjmDbtp3o3HMAR46exMGDR7H3u0PYsaOTrHQXtm7djg7W9%2b0/gkOHjmHPnv3YsmU79u07jGPHTuHo0VM4fPg4Ojp2Yzvnkf47duzB/gNHqeBbD2wJv4oCrpJyH%2bahFi78Gh9/9AnmzVuAzz%2bdhtXrNuGzTz/H1CkfYNZXszF37nzsO3gCx3ngNavXYVXrOkz79DPMY/tnn3%2bJla3rOe5zTHnvfcwkWZo7ZwGOHD/TTbV7rQIkChw6eAxt6zdj5TdrsHLlWqxdu4kWsRubN32LDRva%2bbwRmzdvU1ZxlBbSzhtev34LvlneitWr12PZ8tXYsrUDW9hnw4atWLNmA/tvx3fs70q2eikInqNP3%2bBGD2L37gN0hePYv/%2bYKjs7D2Dv3sN8d5jlIezatU/JkSPHVbl79366wFHV/%2bDB43SLQ3ShQ2w7osbs2rUfO3fsVXjQqy1Ass5D9Pkli5dhyZIV9ONdaGUesKV9J%2bbPXYAZX87EokVLeMursHPnXuXvO9ln8ZJvsH7dBrR/u4O33c6EaTdWLP8Gc2bPY74wF8uWLse2jk4Fkr0bA5gFHqNZy2G%2bnD4Ds2fP5SHmY2v7DnWIBQsWo7V1DWbxYAKSAnrbv%2b3AqlVrsXjRYsyZuwgfvv8Rk6fVWLd2PfFirlLWrK/moJ1u1KsV4AqDgvArV7Whjf6%2bes1GpsDtCgc2bqJPb/yWfr6RskGhv7hLR8de5eNb23ehnZYieLFtWyeR/ztl%2btK2h8qSiHHlytXe7wJtBK5585bQrFsJYJvR1raVKL%2bRtzifFvE1I8RyLF22RoXL/Qx9bW1bsHPXdzzgcfr/MSWbNm4lMG5WYNn%2b7a5u338YMvSrKODK1asK7T9hvv/Ht/%2bMTz%2bZhpZhzXjnr1Pw0fsf44MPPsWHlEWLVzE5Oo02usqKb1Zj1syvMG/%2bQnz08adU3iIsXLAAc%2bbMx0y6gESCa9duPHTO8Mu7QFcY3LipnTe9CNOnz6bPL0ND/WAMHToU8%2bcv4YEWYD7blixtJQCeINh1Yh1veh1Do7iJhM2NdJONPPRacofNWzqUC9yP/r06DHbSt7dt78QehjuR7/YdpCusx/aOPfTrvcrHt2/fTbZ3TIXH3bv3qRB5%2bPBJHKLs33%2bUPn9QibwXjBD/7/W5gIsJHuHBljLfX7hoKQ/bocLZZhKbebPnqLaFCxfT5%2bVQJ7Bv734sZ3RoJRtsJWBuaNvId50csxKffTYdK1asYkRY%2bxtSAMOgbHbt2jbMmD6TNFZo70KsI9CJIlpJeWfOmMVI4eT2ezq/Q%2buqNeqgX0yfhS8%2b/wJfL15Oi2nDcoa/RVTW0mWraB0nfjvZ4Jkz51Uyc%2brUORw/fhqnT59neUqxPmk/cuQEDrMun%2bxPnjqrnoUPnDx5hnIWJ06cURRZFHnsmLM8c%2bbcb0cBrnbXhw4p79y5/8PGvR837va70z3u51Lf35QCHuXHjQf9BtCrFPCo5aEt4HH9yenv/mnszFkVBh/Xn%2bAe%2bk9j%2bIV/8hfZ27e/R2/5ud2gOXbLrZ/Wb/K2frbtlrO8573r%2bVbPuW4okxe5ffuW%2btP42QtnVLt867vZQ27cuN7dJnOJXOshV7tKtU7XesIq5ROY%2bs5IKnzx8nXcuuV8J6W0ufpeu8421z5vOsUtPSMTaUkpSEtORVoiy7R0pKWmsZ6s2lKzs53vRJKc71OzstX71PQMpGZkON9Ju3rOdM4jfZPYzufElBTEJyWrMoX9mgrHIz%2brGAlpSYhPTVRlQmoS0tIzkZSWop6T09KQwvlyOX8OJZtSRMmjyDoJnC%2bZc2Vyf7HxCUhMzUBFaQ5GDs5HTCLPwH6xSekoKsjmmhnIyMxEbm4WElKc71K7xM3D4oenStLRJyseT1Zk4WlHGJ5OjEKf/tl8zsETVXnoU5CKPvnJ6JNHyU1Cn/JstqXgydIMtqWosc8E29GnKA1P1hSiD9ufqCnAU6XpeMLTAykZ2aipa0BxcSGe6uuB8sAWGDVmhJlSUGpvQrqlP1L8iliWI9Mq9RJEmRLxFMcm6DRI0HsjnpJBSaO4u3vwcKnIzctFQUk5KqsG4t%2be8UJhihmb/xKFQIsWOr0GjYVWTBhoQ1qUEXlJvphcb4fRwPFeXujr6RQ3vb8NT8fHQpvogFdaHLxtVngH2uHJuiY1HpqYcGj4TpPMPvFR0FBB2qhQaJKiVKmN5vv4COhMRmhjI9kvhv2joUlxcM4oeOt1iImNQzq17YgMh1FnQoJ/Bry07gj2jUCifzbirLwtWxrspn6I8EtGpCUJvkYr3HU6JPj5ItjoAxPH2U0%2bSLaYodUbEBkTi9CwMEQ44lBcWgJPvk%2bOteGNYf2Q6rByPz7ITrQhM96GZIc/EqP9VT063AID3%2bm7xM1uMyM/hpN662A2%2biIsPBL9wvvBZuYkXhoYg6KhN5qho7Z0BjN8HJkwBvSDwT8MBksgDNYg1kNhCKISNDrofe0wcYzBGgqfyDQYA8Ph7esPT86hDU%2bAT3QqDHa%2bi8%2bDIToJmggqNzAY7hpPhEfz9sKCYLLSOqKiEO2IRl%2bNHnFRVmQn%2bMNi8YGXwQid0QQvjRZeOj0VqeNNejvbdEY86a6Hh1b6GNHX26DqIp4Ud41B9dEaTDyLlEa4JThsmDouEu7eWlRUVSOvqARVtXVoGTkCRh8fWEqbYQp3QKfTQm8JgKX/aASOeRchr82GbchrsA//A/xyaxA46h0qwgZDWByslWPgX/cKzPn1LF%2bCObUYfplVCBo3VT0HsK%2blvIXjBsGS3wBLRgU8uH4RzXns%2bPFobhmBSS9Owugxo2DwMSMj3oqX6sMwaVAID2zkzZnU7cmhRQw%2bvupZblb27LphZ2lSYpD2rncyRtulSDdpNBoMSiPe1Ki7lzc0rPtarPCklj28NUqcdaemvalBbXA0tGberMEHHvQpDy21q9GoNr0tGBqaqdqchc%2bc35s35W0yOxdnqVWlL7RUqt4WBE/OLevLexkrIs9Sarv2ZvIxcB/ci8zFZ626RYPas/iz7NNpEV7KKmTPMt5Lq%2b16L/6v6T68sgC93QavdPotO8UTLZPTUpFDcMkrKCRipiEnNxdZlILCQmTl5PIgWgSFhMLf3599ilBbU420zCzWC1BVXYOg4CD4mC3IzS/gPHmorK5GINuiE5JRX1uN33l4ISs7F2aCb1hEJBITE%2bHOzYXQ7fwDAoj8mSgsLuH4PIXSVTW1sFotavyguno0NrdgcGM9KgYMgD0oGEY/K0rKK9SYfO4xMTkZeYVFxIVSFSHC%2bkUggVFK%2bhcUl6KkuIhW4tvDAmw2aAl4Xh4alJRWoqSoDCkMYbGJScjOL0J5//5KIQncaEZWLi1FDxt9NjA0HHHcVGVVFQpLSlExcCDyeOiAIPqwnz%2bfq1BcUox4zhNAhaVSSRUV5QjiuFwqLjY%2bXoXGjKwsePCmwiOjqIBAVA%2bq50EbMLCyEpHR0VRCFpVtRUi/aNQNqkElFTKA8/TnofuFhVPZVtQOYltlFbKyMhEe5UB/1mvY1xEbi6CwfsigwseOHYem4SPR3NSg3KNbAXoCi3e4FtoAgkQoTSnYGx4WDW/aCTBiRiKuupaILLdlpeLErJ9hSJKJfMx%2bdAVxHxMCgoO7%2buuUeQoii4vJ7btMWrkSzVjE6GvuXkfaBQ/6ejlNWMbb7IEwhwTjGUaAvryAvn5meATaoAkJJIDa4c7IZbMHqP4yn5i7jJM9yFqudh337nKvuy4QbIJ7jQZ9m3nwZzVwL/OGT6UFyfFpDC3ae8BGkFXv64fawY14dsRzqKH5j50wUYWhusFNGNEyHKPHP4/qmkoe2hfDR4xQ/ZqahtCN8jC0eTiebWlBfUO9woXs/GKMJdAVlVWgeXgzqmtrMXhIk%2brXNHQo60OoWF%2bkk%2bREFOfDIzsJuqwUaGqKoM1JhqYsF5oKumVxBi11AExUpBzKFeJ6gp2rru1Rd1qAHy0glLfiz5c%2bIgboA4n%2bVvtdTXWJE6CMsPJGrNR4BE00hqYsIGMNoJ/T5CIdMbAHBqlQE8VYHUOWFhAUCD39Tnw%2bOiYGflYr%2bvyuL%2bLIGOPi4tS88i4kPFyVMkdktEONF%2bAKpqnbw8OUBXgEB6jb9/ChpfgSgO3%2b8KJI/57gdvfS/vGzm97mA89sHYGQJltMM8ykGSaY4G%2bleRkM3TfvQmeJDiVl5YqmRjhiiQ1Jit4mEmiiuHE5QBBdQFyljLfSLzKSinJQKYFUlF7Vo2Pi2NeBFFJuR2w8KXIqAokT4lrJBOKYuHjiSyKCQ0ORRCA2%2bfpyXZIZob%2bk4A76eWRUNMdz3YQEhHBsBNt8zGYSL8PPHvrviZs%2biC5QyVBXS2Hpnu0NU7kFSXGpDDf3uoBGr4eP1YaXJ0/G5NdfJ9hUE9FzCG4VyMjJp0vU4o233sbg%2bkGKpJRWDFAo3tLSTIvyh39QKCZMnIhXX38TLzw/EfX1teTlWcgpLCHw1aFxaDNGjBzFyJOnkH0IXWf02PEIDQ1BREy8co3y/gMxZtxYDKqvQzpzkmpylvHPT8C48RORnJIMXz9Lt%2bU%2bkAIMNCONlQO0wo6oOV/xE4r%2brjmJObs0KpO74q4AoICemYcTMiJt/nSFoJAQ1VeUJwAk/Ty7AElitbiQiaAp84hrabr4h7yT0lun635%2biq4i4GniwYy0BJlLxkgp6yl%2bwLnMtEzZW0%2bEfygLcG%2bkDOXBSr1hKDMjul8sb1F33wAjNDRtYWry/3ui/dpB1YzZhchnjC0uLUN2Xj7qG5tQQfMXVykhQDYNG44kmq8PNylhbGBVDcoZyqoYvgqKilWiVFhUQCAdQvobBVtwGPozrBaVlClQbXl2OAYMHIDS8gG8%2bcFoaGhAbUMj0tPTfgJud/38LlvU9%2bD%2b94MjkyHy60Qyq3iGpCiGvlAdjHHk4qER3czsLgAaqG2LSj/ruAEx75ycLPp9lIrvSfTp9Mxs5JAPRBPAAkPDVJzPpkkHBYfAyJsSsxUMEOKUS7GQPseST5SUl6GxaZiK%2bUbG9nSmr0Jg8opKMWzYMCQnJyGO/CSb5EqwJpNzyhoCqHIYlzW5QquedVf4E0sSZuhiiPeCoJUKiCXlDCH1jONgBylmiBFGSV6Mxp%2bAoJFAExAcqjbuy0gRSMATOi08QMKQuINka2LyVpudGzAo95B2f0YOYZE%2bNGcXD5DNiAm7XCI6LkEpWJQawYRI5hbgFeUFUokClBIlQsnwBIiTUlIUbxDLk9BqDwnnGsEkVzoFprKug3Nm5eQo5WeQLBnuIULBDCVVJBzDvJ1uUOgN30orUhPSlQu4Eg3Xhq1kazX1jRg1aqRKnISeDqS5ilvUkurWNjQpuinj6gY3oJzmPm78BMb55zB8WBPKB1TSjIdi8muvoYhm78%2bQOXLc8xg36jmF9kNbRuHd997DS6%2b8gldefRXTvpyB50aOVFS2qmaQYnljmDDJ/xOPHjOWaw9AckY23p0yBfl5OaisG4xhQxupyERMeOF5rlWPF156BVOnvoepH/0NH055pxsvnAowm9DXRrOwOHmAp5EA5KOhCfkSfDxU8uAEJr26WQllAmKiRWFaEt/9aA3CC3wtzpsVAPXqiiAWWoEgs5FZnYQp5ZusC1%2bPT4hXFiIh0I83Jf3ltmWMiI3KEfeRWwwmpQ2PiICdViD9xQpsAU4wlZxArEuotHAG4RPSJlYiVuhHCeA4a1cf12VK3S0oPAo1vCUP%2bkZYZAwGEZjqeasxJCiSPGTSh4XFhYZxk/YglJaVUsqZLo9CFfMA2ZT4fjHbNLQYBzVfUlZGkpOo6G%2bcfDpjUhNHf5bwVlBUpMzWYHJSXpXHU5EuiuzKAkUxLlrrzOic1Nq76xuA%2bLUrW3T5veudV1e26MQAQ3e7q49rTVGkW3B4JEaNHqnSzBiCTEFhPpOHbKQyKxxEVK6qqlSoLiBktgUSrQvpo3FMdipV9peVk82kJkXFbT2xIrRfFOoHD1Ymq6NPikm%2b%2bPKLyMorYGKUqLI2ySqtdvsDxet7qfg/b39QEQWIVbiF5tciRQiE5O1d4UHlztReXw9PZf6Srnp1aVtcwOu%2bhMO963uBy7RcMVzdjtxsF3dwJVUm5hOaf8LY7ufw/4ja/ssKEAtw1L2IGMZeQXZp7Knd%2b2Pn/TfQs1/Pjd0/pmdMdkWTR3WQ/7MCEhtfRk11JWwEje64/5C38uD9jY/0Fh%2bFC/wvaziYeObNTrkAAAAASUVORK5CYII='%20/%3e%3c/svg%3e" alt="Prova" data-srcset="https://d33wubrfki0l68.cloudfront.net/5c6f9d7829ced2d6df1a2953744acf0d28b764f6/73e0c/assets/static/sse-auto-reconnect.82a2fbd.b190f19887f331ddb680b6ba6bc4921e.gif 480w, https://d33wubrfki0l68.cloudfront.net/5c6f9d7829ced2d6df1a2953744acf0d28b764f6/24679/assets/static/sse-auto-reconnect.7900e78.b190f19887f331ddb680b6ba6bc4921e.gif 836w" data-sizes="(max-width: 836px) 100vw, 836px" data-src="https://d33wubrfki0l68.cloudfront.net/5c6f9d7829ced2d6df1a2953744acf0d28b764f6/24679/assets/static/sse-auto-reconnect.7900e78.b190f19887f331ddb680b6ba6bc4921e.gif"><noscript><img class="g-image g-image--lazy g-image--loaded" src="https://d33wubrfki0l68.cloudfront.net/5c6f9d7829ced2d6df1a2953744acf0d28b764f6/24679/assets/static/sse-auto-reconnect.7900e78.b190f19887f331ddb680b6ba6bc4921e.gif" alt="Prova"></noscript></p>
  490. <h2 id="conclusion">Conclusion<a aria-hidden="true" tabindex="-1" class="h-anchor" href="#conclusion"><span class="icon icon-link"></span></a></h2>
  491. <p>WebSockets are a big machinery built on top of HTTP and TCP to provide a set
  492. of extremely specific features, that is <strong>two-way</strong> and <strong>low latency</strong> communication.</p>
  493. <p>In order to do that they introduce a number of complications, which end up
  494. making both client and server implementations more complicated than solutions
  495. based entirely on HTTP.</p>
  496. <p>These complications and limitations have been addressed by new specs (<a href="https://tools.ietf.org/html/rfc7692" target="_blank" rel="nofollow noopener noreferrer">RFC 7692</a>, <a href="https://tools.ietf.org/html/rfc8441" target="_blank" rel="nofollow noopener noreferrer">RFC 8441</a>), and
  497. will slowly end up implemented in client and server libraries.</p>
  498. <p>However, even in a world where WebSockets have no technical downsides, they will
  499. still be a fairly complex technology, involving a large amount of additional code both on clients and servers.
  500. Therefore, you should carefully consider if the addeded complexity is worth it,
  501. or if you can solve your problem with a much simpler solution, such as Server-Sent Events.</p>
  502. <hr>
  503. <p>That’s all, folks! I hope you found this post interesting and maybe learned
  504. something new.</p>
  505. <p><a href="https://github.com/tyrion/sse-websockets-demo" target="_blank" rel="nofollow noopener noreferrer">Feel free to check out the code of the demo on GitHub</a>, if you want to experiment
  506. a bit with Server Sent Events and Websockets.</p>
  507. <p><a href="https://html.spec.whatwg.org/#server-sent-events" target="_blank" rel="nofollow noopener noreferrer">I also encourage you to read the spec</a>, because it surprisingly clear and contains
  508. many examples.</p>
  509. </article>
  510. <hr>
  511. <footer>
  512. <p>
  513. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  514. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  515. </svg> Accueil</a> •
  516. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  517. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  518. </svg> Suivre</a> •
  519. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  520. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  521. </svg> Pro</a> •
  522. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  523. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  524. </svg> Email</a> •
  525. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  526. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  527. </svg> Légal</abbr>
  528. </p>
  529. <template id="theme-selector">
  530. <form>
  531. <fieldset>
  532. <legend><svg class="icon icon-brightness-contrast">
  533. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  534. </svg> Thème</legend>
  535. <label>
  536. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  537. </label>
  538. <label>
  539. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  540. </label>
  541. <label>
  542. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  543. </label>
  544. </fieldset>
  545. </form>
  546. </template>
  547. </footer>
  548. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  549. <script>
  550. function loadThemeForm(templateName) {
  551. const themeSelectorTemplate = document.querySelector(templateName)
  552. const form = themeSelectorTemplate.content.firstElementChild
  553. themeSelectorTemplate.replaceWith(form)
  554. form.addEventListener('change', (e) => {
  555. const chosenColorScheme = e.target.value
  556. localStorage.setItem('theme', chosenColorScheme)
  557. toggleTheme(chosenColorScheme)
  558. })
  559. const selectedTheme = localStorage.getItem('theme')
  560. if (selectedTheme && selectedTheme !== 'undefined') {
  561. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  562. }
  563. }
  564. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  565. window.addEventListener('load', () => {
  566. let hasDarkRules = false
  567. for (const styleSheet of Array.from(document.styleSheets)) {
  568. let mediaRules = []
  569. for (const cssRule of styleSheet.cssRules) {
  570. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  571. continue
  572. }
  573. // WARNING: Safari does not have/supports `conditionText`.
  574. if (cssRule.conditionText) {
  575. if (cssRule.conditionText !== prefersColorSchemeDark) {
  576. continue
  577. }
  578. } else {
  579. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  580. continue
  581. }
  582. }
  583. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  584. }
  585. // WARNING: do not try to insert a Rule to a styleSheet you are
  586. // currently iterating on, otherwise the browser will be stuck
  587. // in a infinite loop…
  588. for (const mediaRule of mediaRules) {
  589. styleSheet.insertRule(mediaRule.cssText)
  590. hasDarkRules = true
  591. }
  592. }
  593. if (hasDarkRules) {
  594. loadThemeForm('#theme-selector')
  595. }
  596. })
  597. </script>
  598. </body>
  599. </html>