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!

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:

  1. Status
  2. Otsakkeet
  3. 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.

Construction by Random Sky / Unsplash

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ä: