HTTP Early Hints ja Phoenix
HTTP-protokollan 2-version selaintuki on jo pitkään näyttänyt hyvin kattavalta, joten alkaisi mielestäni olla aika päästä oikeasti hyödyntämään sen mukanaan tuomia ominaisuuksia. Kakkosversio parantaa protokollaa monin tavoin ja antaa muutamia erittäin hyödyllisiä työkaluja verkkopalveluiden käyttöön.
Yksi HTTP/2:n suurimmista lupauksista oli server push -ominaisuus, eli palvelinaloitteinen tiedonsiirto. Sen avulla palvelin voi proaktiivisesti lähettää asiakasselaimelle tietoa, jota selaimen oletetaan joka tapauksessa tarvitsevan. Tällaisia olisivat esimerkiksi selaimen pyytämässä HTML-dokumentissa viitatut CSS- ja JS-tiedostot.
Niin hieno kuin palvelinaloitteinen tiedonsiirto ajatuksena onkin, se ei todellisuudessa ole ihan niin hyödyllinen kuin voisi äkkiseltään kuvitella. Palvelin ei nimittäin tiedä, onko selain tai jokin matkalla oleva välityspalvelin tallentanut tarvittavat resurssit jo välimuistiinsa, jolloin palvelin joutuu lähettämään ne aina uudelleen – pääsääntöisesti turhaan. Lopputuloksena siis sivun ensimmäinen lataus tapahtuu nopeammin kuin se olisi ilman palvelimen oma-aloitteisuutta onnistunut, mutta kaikki sivulataukset sen jälkeen kuluttavat yleensä vain turhaan kaistaa, kun palvelin siirtää samat tiedostot aina uudelleen. Lisäksi tästä koko hommasta on hyötyä vain silloin, kun resurssien lähteenä toimii sama palvelin, eikä niitä ladata esimerkiksi CDN:stä tai jostakin muusta ulkopuolisesta palvelusta. Olisipa tähän joku järkevämpi ratkaisu!
Teknologia: Lue lisää
HTTP 103 Early Hints
HTTP-protokollan 1.1-versioon on vuonna 2017 ehdotettu lisättävän statuskoodi 103, eli Early Hints. Tätä statusta on tarkoitus hyödyntää siten, että selaimelle voidaan jo ennen varsinaista vastausta kertoa, mitä resursseja selain tulee tarvitsemaan lopullisen vastauksen kanssa. Tämä on siinä mielessä hiukan erikoinen ajatus, että palvelin lähettää siis selaimen pyyntöön useamman vastauksen: yhden tai useamman Early Hints -vastauksen ja lopuksi varsinaisen dokumentin, jossa noiden aiempien vastausten määrittelemiin resursseihin viitataan.
Tavallinen HTTP-vastaus on rakenteeltaan simppeli:
- Status
- Otsakkeet
- Data
Niinpä yksinkertaista HTML-dokumenttia ladattaessa palvelimelta voisi välittyä vastaus esimerkiksi seuraavanlaisena:
HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Connection: keep-alive Vary: Accept-Encoding <!DOCTYPE html> <html> <head> <link rel="stylesheet" href="/styles.css" /> </head> <body> <h1>Tämä on palvelimelta ladattu HTML-dokumentti</h1> </body> </html>
Tässä vastauksessa HTTP/1.1 200 OK
kertoo käytetyn HTTP-protokollaversion ja statuksen, joka on siis koodi 200, eli OK. Content-Type
-, Connection
– ja Vary
-alkuiset rivit ovat HTTP-otsakkeita, jotka kertovat erilaisia tietoja itse dokumentista sekä siitä, miten dokumentin kanssa tulisi toimia. Loppuosa onkin sitten itse HTML-dokumentti.
Tilanne muuttuu hieman erilaiseksi, kun CSS-tiedoston viittauksen kanssa käyttöön otetaan 103-status:
HTTP/1.1 103 Early Hints Link: </styles.css>; rel=preload; as=style HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Connection: keep-alive Vary: Accept-Encoding <!DOCTYPE html> <html> <head> <link rel="stylesheet" href="/styles.css" /> </head> <body> <h1>Tämä on palvelimelta ladattu HTML-dokumentti</h1> </body> </html>
Nyt palvelimelta tulee kaksi vastausta yhdellä kertaa: etukäteen lähetettävä 103-vastaus sekä itse dokumenttia koskeva 200-vastaus. 103-statuksen kanssa ei ole tarkoitus käyttää muita kuin Link
-otsakkeita, mutta niitä voi olla useita yhden vastauksen sisällä. Mitä hyötyä tästä sitten on?
Yllätyksenä minulle kuitenkin tuli se, että Early Hints -toteutuksen rakentamisesta Phoenixille en löytänyt Internetistä juuri mitään: ei dokumentaatiota, ei tutoriaalia, ei blogikirjoitusta, ei edes keskustelua! Tämä olikin päällimmäinen syy, miksi halusin kirjoittaa tämän blogikirjoituksen.
Early Hintsin hyödyt
Vaikka vastaukset palvelimelta tavallaan tulevat yhdessä köntissä samaan pyyntöön, voi Early Hints -vastausten ja varsinaisten dokumenttivastausten välillä kulua määrittelemätön määrä aikaa. Koska Early Hints -vastauksen on tarkoituksena kertoa selaimelle, mitä resursseja selaimen tulisi ainakin ladata, voidaan se lähettää välittömästi palvelimelta käytännössä aina. Varsinaisen dokumenttivastauksen koostamiseksi puolestaan joudutaan usein tekemään milloin mitäkin prosessointia, joten tuon vastauksen muodostamisessa saattaa kestää helposti pitkäänkin. Ilman Early Hints -vastausta tuo aika menee selaimen näkökulmasta hukkaan.
Early Hints myös korjaa kaksi palvelinaloitteisen tiedonsiirron ongelmaa: turhan tiedonsiirron sekä toisilta palvelimilta ladattavat resurssit. Koska Early Hints -vastauksessa selaimelle kerrotaan vain ne resurssit, jotka selaimen tulisi ladata, mutta ei suoraan pakoteta resurssien dataa selaimen kurkusta alas, voi selain ongelmitta hyödyntää omaa välimuistiaan. Early Hints -vastauksissa resurssiviittaukset voivat osoittaa minne vain, joten toisista domaineista ladattavat tiedostot toimivat juuri niin kuin pitää.
Kaiken muun hyvän lisäksi Early Hints toimii HTTP-protokollaversion 1.1 kanssa, joten sinällään tukea HTTP/2:lle ei tarvita palvelimelta eikä asiakasselaimelta. Tässä etuna on oikeastaan vain se, että HTTP/1.1:n kanssa ei ole pakko käyttää salausta, kun taas HTTP/2:n kanssa se on selaintuen vuoksi pakollista. HTTP/2-protokolla ei sinällään vaadi salauksen käyttämistä, mutta yksikään selain ei taida tukea HTTP/2:ta salaamattomana.
Early Hints -toteutus Phoenix-sovelluskehyksellä
Halusin kokeilla Early Hints -vastausten rakentamista, mutta PHP:lla homma tuntui varsin mahdottomalta. Ensimmäinen ongelma on, että PHP ei itsessään tue useamman statuksen lähettämistä saman vastauksen sisällä. Toinen ongelma taas on, että tuota samaista tukea ei löydy kovin valmiina myöskään rajapinnoista, jotka ovat PHP:n ja HTTP-palvelinten välissä. Vaaditaan siis merkittäviä kehitystoimenpiteitä useaan avoimen lähdekoodin projektiin, jotta Early Hints -vastaukset saadaan toimimaan PHP:n kanssa. Koska kyse on edelleen kokeellisesta standardista, ei noita kehitystoimenpiteitä olla kovin suurella innolla edistämässä. PHP on siis toistaiseksi poissa laskuista Early Hints -toteutusten rakentamisessa.
Suurin osa työstämme Karhu Helsingissä pyörii PHP:n ympärillä. Olen kuitenkin vapaa-aikanani jonkin verran puuhastellut Elixir-ohjelmointikielen ja sille rakennetun Phoenix-sovelluskehyksen parissa. Mielestäni sekä Elixir että Phoenix ovat hyvin, hyvin vaikuttavia ja Phoenixin kanssa tuntuu, että käytössä on jonkinasteisia web-kehittäjäsupervoimia. Onnistuisiko Phoenixilla Early Hints -toteutuksen rakentaminen jotenkin järkevällä työmäärällä?
Phoenix tuntuu etenkin PHP-kehittäjän näkökulmasta suorastaan epäreilun voimakkaalta ja monipuoliselta web-sovelluskehykseltä. Loppujen lopuksi Early Hintsinkin käyttäminen Phoenixin kanssa on lähes triviaalia jopa kaltaiselleni Phoenix-aloittelijalle. Yllätyksenä minulle kuitenkin tuli se, että Early Hints -toteutuksen rakentamisesta Phoenixille en löytänyt Internetistä juuri mitään: ei dokumentaatiota, ei tutoriaalia, ei blogikirjoitusta, ei edes keskustelua! Tämä olikin päällimmäinen syy, miksi halusin kirjoittaa tämän blogikirjoituksen. 🙂
Phoenixin HTTP-palvelimena toimii Cowboy. Cowboy toteuttaa kutakuinkin täydellisesti HTTP-protokollan versiot 1.1 ja 2 – sisältäen tuen Early Hints -vastauksille. Kaikki Phoenixin toiminta Cowboyn kanssa puolestaan rakentuu Plug-adapterin varaan. Tämän adapterifunktion lisäksi Plug toimii rajapintana selkeiden ja yksinkertaisten moduulien rakentamiseen ja koostamiseen web-sovelluksissa. Näitä moduuleja kutsutaan plugeiksi ja ne voivat periaatteessa tehdä mitä vain, mutta erittäin käytännöllisiä ne ovat HTTP-pyyntöjen käsittelyssä ja vastausten rakentamisessa.
Ei liene yllätys, että Early Hints -tominnallisuus kannattaa Phoenixin kanssa toteuttaa plugina. Plugeja voidaan rakentaa joko yksittäisinä funktioina tai Elixir-moduuleina. Koska tässä tapauksessa voidaan suoraan hyödyntää Plugiin itseensä rakennettua Early Hints -tukea, riittää yksinkertainen funktiototeutus erinomaisesti. Rakennettavan plugin toteutus näyttää seuraavalta:
def send_early_hints(conn, headers) do Plug.Conn.inform(conn, :early_hints, headers) end
Oikeasti! Siinä se!
Tätä uutta plugia voidaan sitten käyttää osana selaimelle tarkoitettua pipelineä seuraavasti:
pipeline :browser do plug :accepts, ["html"] plug :send_early_hints, [{"link", "</css/app.css>; rel=preload; as=style"}] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end
Kokonaisuudessaan Phoenixin esimerkkireititin (projektin web-rajapinnan router.ex
-tiedosto) muuttuu siis seuraavaan muotoon:
defmodule ExampleWeb.Router do use ExampleWeb, :router pipeline :browser do plug :accepts, ["html"] plug :send_early_hints, [{"link", "</css/app.css>; rel=preload; as=style"}] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end pipeline :api do plug :accepts, ["json"] end scope "/", ExampleWeb do pipe_through :browser get "/", PageController, :index end # Other scopes may use custom stacks. # scope "/api", GiphyWeb do # pipe_through :api # end def send_early_hints(conn, headers) do Plug.Conn.inform(conn, :early_hints, headers) end end
Olen täysi aloittelija Elixirin ja Phoenixin kanssa, joten todennäköisesti ongelman saa ratkaistua jotenkin vielä elegantimmin. Mielestäni tämä ratkaisu kuitenkin on jo niin suoraviivainen, että kauheasti parannettavaa en keksi. Toteutus toimii ongelmitta ja toiminnan testaaminen onnistuu helposti lisäämällä ExampleWeb.PageController
-moduulin index
-funktioon odotusaika selventämään näiden kahden vastauksen keskinäistä riippumattomuutta:
defmodule ExampleWeb.PageController do use ExampleWeb, :controller def index(conn, _params) do Process.sleep(3000) render(conn, "index.html") end end
Pyydettäessä etusivua esim. curl
-komentorivisovelluksella, tulee Phoenixilta välittömästi Early Hints -vastaus ja kolme sekuntia myöhemmin varsinainen dokumenttivastaus:
$ curl -I http://localhost:4000/ HTTP/1.1 103 Early Hints link: </css/app.css>; rel=preload; as=style # Aikaa kuluu... HTTP/1.1 200 OK cache-control: max-age=0, private, must-revalidate content-length: 2024 content-type: text/html; charset=utf-8 cross-origin-window-policy: deny date: Wed, 22 Apr 2020 08:06:12 GMT server: Cowboy x-content-type-options: nosniff x-download-options: noopen x-frame-options: SAMEORIGIN x-permitted-cross-domain-policies: none x-request-id: FggVvQP1I6YSA0MAAAIF x-xss-protection: 1; mode=block
Yhteenveto
Early Hints on erittäin hyödyllinen ominaisuus, jonka suurin ongelma on puuttuva tuki niin selain- kuin palvelinpäässäkin. Niin kauan kuin tuki selaimista puuttuu, ei palvelinpään toteutuksille ole tarvetta. On kuitenkin hienoa, että ainakin Phoenixilla Early Hintsin saa käyttöön palvelinpuolella erittäin pienellä vaivalla. Selaimissa tuen rakentaminen on todella monimutkainen juttu, mutta edut ovat kiistattomat. Chromiumin ja Firefoxin edistystä Early Hints -tuen toteutuksessa voi seurata selainten omissa bugiseurantajärjestelmissä:
- bugs.chromium.org: Issue 671310: Support 103 Early Hints in HTTP/2
- Bugzilla: Bug 1407355: Implement 103 Early Hints