Hypertext Transfer Protocol (HTTP-protokolla)
Käydään läpi HTTP-protokollan tärkeimmät osat webbisovellusten ja sovelluskehityksen kannalta. Tähän liittyen käsitellään myös www-lomakkeiden toiminta.
HTTP-pyyntö ja -vastaus
HTTP on protokolla, jonka avulla selain ja www-palvelin keskustelevat. Keskustelu koostuu pyynnöistä ja vastauksista: selain pyytää yhtä resurssia kerrallaan, ja palvelin vastaa kuhunkin pyyntöön erikseen.
Sekä pyyntö että vastaus koostuvat kolmesta osasta: aloitusrivistä, otsakkeista ja mahdollisesta rungosta. Otsakkeet ja rungon erottaa tyhjä rivi. Tämä on syytä painaa mieleen, koska CGI-ohjelmassa juuri tuo tyhjä rivi on tulostettava itse.
Selaimen ja palvelimen välistä liikennettä on helpointa seurata selaimen kehittäjätyökalujen (F12 tai SHIFT+CTRL+I) verkkovälilehdellä.
Yhden sivun avaaminen ei ole yksi pyyntö vaan monta. Selain hakee ensin dokumentin, jäsentää sen ja hakee vasta sitten kaiken, mihin dokumentti viittaa: tyylitiedostot, skriptit, kuvat ja fontit. Jokainen niistä on oma HTTP-pyyntönsä omine otsakkeineen ja statuskoodeineen.
Selaimen lähettämä pyyntö
GET /ties4080/luennot/http/ HTTP/1.1
Host: appro.mit.jyu.fi
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: fi,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate, br
Referer: https://appro.mit.jyu.fi/ties4080/luennot/
Connection: keep-alive
Upgrade-Insecure-Requests: 1
| Rivi | Merkitys |
|---|---|
GET /ties4080/luennot/http/ HTTP/1.1 |
Pyydetään resurssia (GET), jonka polku palvelimella on
/ties4080/luennot/http/, protokollaversiolla HTTP/1.1. |
Host: appro.mit.jyu.fi |
Palvelimen nimi. Pakollinen HTTP/1.1:ssä, koska samassa IP-osoitteessa voi olla useita sivustoja. |
User-Agent: … |
Selaimen tiedot. Älä rakenna sovelluslogiikkaa tämän varaan; arvo on helppo väärentää ja se valehtelee historiallisista syistä muutenkin. |
Accept: … |
Selaimen hyväksymät mediatyypit ja niiden painotus
q-arvoilla. Palvelin voi valita tämän perusteella, minkä
muodon se palauttaa (sisältöneuvottelu). |
Accept-Language: fi,en-US;q=0.7,en;q=0.3 |
Halutut kielet paremmuusjärjestyksessä. |
Accept-Encoding: gzip, deflate, br |
Tuetut pakkaustavat. Palvelin saa pakata vastauksen näillä. |
Referer: … |
Osoite, josta ollaan tulossa. Kirjoitusvirhe otsakkeen nimessä on alkuperäisestä speksistä eikä sitä ole koskaan korjattu. |
Connection: keep-alive |
Jäänne HTTP/1.0-ajalta. HTTP/1.1:ssä yhteys on oletuksena pysyvä, joten otsaketta ei tarvittaisi. |
Accept-Charset-otsake on poistettu käytöstä eivätkä selaimet
enää lähetä sitä. Merkistö kerrotaan nykyisin vain vastauksen
Content-Type-otsakkeessa.
Palvelimen vastaus
HTTP/1.1 200 OK
Date: Tue, 08 Sep 2026 10:53:18 GMT
Server: Apache
Content-Type: application/xhtml+xml; charset=UTF-8
Content-Length: 286
Last-Modified: Mon, 07 Sep 2026 12:44:13 GMT
ETag: "11e-63f0a1c2e4b80"
Accept-Ranges: bytes
Vary: Accept-Encoding
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="fi" xml:lang="fi">
<head>
<meta charset="UTF-8" />
<title>Testisivu</title>
<link rel="stylesheet" href="testi.css" />
</head>
<body>
<h1>Testiotsikko</h1>
<p>Testitekstiä</p>
</body>
</html>
| Rivi | Merkitys |
|---|---|
HTTP/1.1 200 OK |
Statuskoodi. Kaikki kunnossa. |
Date: … |
Vastauksen lähetysaika. Aina GMT-ajassa. |
Server: Apache |
Palvelinohjelmisto. Versiotiedot kannattaa piilottaa, koska ne helpottavat tunnettujen haavoittuvuuksien etsimistä. |
Content-Type: application/xhtml+xml; charset=UTF-8 |
Mediatyyppi ja merkistö. Tämä on tärkein otsake, jonka oma sovellus asettaa: selain tulkitsee sisällön sen perusteella. |
Content-Length: 286 |
Rungon koko tavuina — ei merkkeinä. Ääkköset vievät UTF-8:ssa kaksi tavua. |
Last-Modified: … |
Milloin resurssia on viimeksi muutettu. |
ETag: "11e-63f0a1c2e4b80" |
Sisällön tunniste, joka muuttuu dokumentin muuttuessa. Selain voi
kysyä If-None-Match-otsakkeella, onko sisältö muuttunut, ja
saada vastaukseksi 304 Not Modified. |
Accept-Ranges: bytes |
Palvelin osaa palauttaa myös osan resurssista (esim. videon kelaus tai keskeytyneen latauksen jatkaminen). |
Vary: Accept-Encoding |
Kertoo välimuisteille, minkä pyyntöotsakkeen mukaan vastaus vaihtelee. Ilman tätä välimuisti voi tarjoilla väärän version. |
Accept, Accept-Language ja Accept-Encoding
Nämä kolme otsaketta ovat toiveita, eivät käskyjä. Selain kertoo niillä, mitä se osaa ottaa vastaan ja mitä se mieluiten saisi, ja palvelin valitsee. Ilmiötä kutsutaan sisältöneuvotteluksi: sama osoite voi palauttaa eri sisällön eri pyytäjälle.
Kaikki kolme käyttävät samaa syntaksia: vaihtoehdot pilkuilla eroteltuna ja
kunkin painotus q-arvolla väliltä 0–1. Puuttuva
q tarkoittaa arvoa 1, ja q=0 tarkoittaa "tämä ei
kelpaa lainkaan". Suurempi arvo on parempi, ja tarkempi määrittely voittaa
yleisemmän: text/html on tarkempi kuin text/*, joka on
tarkempi kuin */*.
| Pyyntöotsake | Mistä neuvotellaan | Vastauksessa | Välimuistille |
|---|---|---|---|
Accept | mediatyyppi | Content-Type | Vary: Accept |
Accept-Language | kieli | Content-Language |
Vary: Accept-Language |
Accept-Encoding | pakkaus siirtoa varten | Content-Encoding |
Vary: Accept-Encoding |
Jos vastaus vaihtelee jonkin pyyntöotsakkeen mukaan, se on
kerrottava Vary-otsakkeessa. Muuten välimuisti — selaimen
oma, yliopiston välityspalvelin tai CDN — tarjoilee ensimmäisenä pyytäneelle
valitun version kaikille muillekin. Klassinen oire: sivusto jää englanniksi,
koska joku haki sen ensin englanniksi.
Accept
Accept luettelee mediatyypit, joita pyytäjä osaa käsitellä.
Selaimen dokumenttipyynnössä arvo on nykyisin käytännössä vakio:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Luettelo tarkoittaa: mieluiten text/html tai
application/xhtml+xml (molemmat painolla 1), sitten
application/xml (0,9), ja viimeisenä mikä tahansa muu (0,8). Selain
siis hyväksyy XHTML:n täysin samanarvoisena kuin HTML:n, mikä on kurssin
tehtävien kannalta olennaista.
Sama selain lähettää eri arvon riippuen siitä, mitä se on hakemassa. Kuvaa
haettaessa arvo on esimerkiksi
image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8 ja
tyylitiedostoa haettaessa text/css,*/*;q=0.1. Palvelin voi tämän
perusteella palauttaa samasta osoitteesta modernin AVIF-kuvan uusille selaimille
ja JPEG-kuvan vanhoille.
Rajapinnoissa otsaketta käytetään muodon valitsemiseen. Sama osoite voi palauttaa koneelle JSON-muotoa ja selaimelle sivun:
from flask import request, jsonify, render_template, Response
@app.route("/lauta")
def lauta():
paras = request.accept_mimetypes.best_match(
["application/json", "application/xhtml+xml"])
if paras == "application/json":
vastaus = jsonify(tilanne())
else:
vastaus = Response(render_template("lauta.html", tilanne=tilanne()),
content_type="application/xhtml+xml; charset=UTF-8")
# kerrotaan välimuisteille, että vastaus riippuu Accept-otsakkeesta
vastaus.headers["Vary"] = "Accept"
return vastaus
Kokeile eroa komentoriviltä:
curl -H "Accept: application/json" https://palvelin.example/lauta
curl -H "Accept: application/xhtml+xml" https://palvelin.example/lauta
Jos palvelin ei osaa tuottaa mitään pyydetyistä muodoista, se saa vastata
406 Not Acceptable. Käytännössä useimmat palvelimet palauttavat
mieluummin oletusmuodon kuin virheen, koska rikkinäinen
Accept-otsake on yleisempi vika kuin aito
yhteensopimattomuus.
Accept-Language
Accept-Language kertoo käyttäjän kieliasetukset
paremmuusjärjestyksessä. Kielitunnisteet ovat BCP 47 -muotoisia: pelkkä kieli
(fi) tai kieli ja alue (en-US,
sv-FI).
Accept-Language: fi,en-US;q=0.7,en;q=0.3
Tämä tarkoittaa: suomeksi jos suinkin mahdollista, muuten
amerikanenglanniksi, ja viimeisenä mikä tahansa englanti. Palvelin valitsee
parhaan tarjolla olevan version, kertoo valintansa
Content-Language-otsakkeessa ja lisää
Vary: Accept-Language.
kieli = request.accept_languages.best_match(["fi", "en", "sv"]) or "fi"
vastaus = Response(render_template(f"etusivu.{kieli}.html"),
content_type="application/xhtml+xml; charset=UTF-8")
vastaus.headers["Content-Language"] = kieli
vastaus.headers["Vary"] = "Accept-Language"
Muista myös merkitä dokumentin kieli
html-elementin lang- ja
xml:lang-attribuuteilla. Ne kertovat kielen ruudunlukijalle ja
hakukoneelle; HTTP-otsake taas kertoo, miksi juuri tämä versio lähetettiin.
Otsakkeen vaikutusta on helpointa kokeilla curlilla. Valitsin -D -
tulostaa vastauksen otsakkeet ja -o /dev/null heittää itse sisällön
menemään:
curl -s -D - -o /dev/null -H "Accept-Language: fi" https://hazor.iki.fi/
curl -s -D - -o /dev/null -H "Accept-Language: en" https://hazor.iki.fi/
Vertaa vastauksista Content-Language- ja
Vary-otsakkeita sekä Content-Length-arvoa. Jos ne ovat
molemmilla kutsuilla samat, palvelin ei neuvottele kielestä lainkaan vaan
palauttaa aina saman dokumentin — mikä on täysin kelvollista, kunhan
Vary: Accept-Language ei silloin lupaa muuta.
Todellisessa selainpyynnössä kieliä on useita painotuksineen. Näin saat curlilla saman pyynnön, jonka suomenkielinen selain lähettäisi, ja voit samalla katsoa millaisen dokumentin kielen palvelin ilmoittaa:
curl -s -D - -o /dev/null -H "Accept-Language: fi,en;q=0.5" https://hazor.iki.fi/
curl -s -D - -o /dev/null -H "Accept-Language: en-US,en;q=0.9,fi;q=0.3" https://hazor.iki.fi/
# pelkkä yksi otsake näkyviin monen sijasta
curl -s -D - -o /dev/null -H "Accept-Language: en" https://hazor.iki.fi/ | grep -i content-language
- Kieli ei ole sijainti. Suomessa oleva vaihto-opiskelija pyytää sivua englanniksi. Älä päättele kieltä IP-osoitteesta.
- Käyttäjän oma valinta voittaa aina. Tarjoa näkyvä kielivalitsin ja muista valinta esimerkiksi evästeessä tai osoitteessa. Moni ei ole koskaan koskenut selaimensa kieliasetukseen.
- Otsake on osa selaimen sormenjälkeä. Harvinainen kieliyhdistelmä yksilöi käyttäjän tehokkaasti, joten sitä ei kannata tallentaa lokeihin turhaan.
Accept-Encoding
Accept-Encoding ei koske sisältöä vaan sen pakkaamista siirtoa
varten. Selain kertoo, mitä pakkaustapoja se osaa purkaa:
Accept-Encoding: gzip, deflate, br, zstd
gzip— vanha ja kaikkialla tuettubr— Brotli, pakkaa tekstiä gzipiä tiukemmin; nykyisin oletusvalinta HTTPS-yhteyksilläzstd— Zstandard, uusin tulokasidentity— ei pakkausta lainkaan
Palvelin valitsee yhden, pakkaa vastauksen rungon ja kertoo valinnastaan:
HTTP/1.1 200 OK
Content-Type: application/xhtml+xml; charset=UTF-8
Content-Encoding: br
Content-Length: 1284
Vary: Accept-Encoding
Selain purkaa vastauksen automaattisesti, joten sivun tekijä ei näe eroa — paitsi siirretyn datan määrässä. Tekstimuotoinen sisältö kutistuu tyypillisesti neljäsosaan tai viidesosaan. Kehittäjätyökalujen verkkovälilehti näyttää sekä siirretyn että purettuun kokoon; niiden ero on juuri tämä.
Kokeile itse. Pyydä sama sivu ensin gzipillä ja sitten Brotlilla ja vertaa,
mitä palvelin vastaa Content-Encoding-otsakkeessa:
curl -s -D - -o /dev/null -H "Accept-Encoding: gzip" https://hazor.iki.fi/
curl -s -D - -o /dev/null -H "Accept-Encoding: br" https://hazor.iki.fi/
Siirretyn datan määrän näkee -w-valitsimella. Vertaa
pakkaamatonta vastausta molempiin pakkaustapoihin:
curl -s -o /dev/null -w "identity: %{size_download} tavua\n" \
-H "Accept-Encoding: identity" https://hazor.iki.fi/
curl -s -o /dev/null -w "gzip: %{size_download} tavua\n" \
-H "Accept-Encoding: gzip" https://hazor.iki.fi/
curl -s -o /dev/null -w "br: %{size_download} tavua\n" \
-H "Accept-Encoding: br" https://hazor.iki.fi/
Huomaa, että curl ei pura pakkausta itse, ellei sitä pyydetä.
Ilman --compressed-valitsinta pakattu vastaus tulostuu ruudulle
roskana, ja juuri siksi edellisissä esimerkeissä sisältö ohjattiin
/dev/null-laitteeseen. Kun haluat sekä pakatun siirron että luettavan
lopputuloksen, käytä --compressed-valitsinta — se lisää
Accept-Encoding-otsakkeen automaattisesti ja purkaa vastauksen:
curl -s --compressed https://hazor.iki.fi/ | head
# vertaa: sama pyyntö ilman purkamista tulostaa binääriroskaa
curl -s -H "Accept-Encoding: gzip" https://hazor.iki.fi/ | head
Jos palvelin ei tue pyydettyä pakkaustapaa, se vastaa yksinkertaisesti
pakkaamattomana ilman Content-Encoding-otsaketta. Sitä ei siis
tarvitse erikseen varautua käsittelemään.
Content-Encoding kertoo pakkauksen ja
Content-Type-otsakkeen charset-parametri
merkistön. Ne ovat eri asioita, vaikka molempia kutsutaan
arkikielessä koodaukseksi. Merkistö kertoo, miten kirjaimet on esitetty
tavuina; pakkaus taas tiivistää nuo tavut siirtoa varten ja puretaan ennen kuin
merkistöä edes katsotaan.
Pakkaus on www-palvelimen työtä, ei sovelluksen. Apachella siitä huolehtii
mod_deflate ja Cloud Runissa palvelun edessä oleva kuormantasaaja,
joten CGI- tai Flask-sovelluksen ei tarvitse tehdä asialle mitään. Jo valmiiksi
pakattuja tiedostoja (JPEG, PNG, videot, zip) ei kannata pakata uudelleen; niistä
ei ole enää mitään puristettavaa.
HTTPS
HTTPS ei ole eri protokolla vaan tavallista HTTP:tä TLS-salauksen sisällä. Se antaa kolme asiaa: liikenne on salattua, sen muuttuminen matkalla havaitaan, ja palvelimen henkilöllisyys on varmennettu varmenteella.
Salaus koskee koko pyyntöä ja vastausta: polkua,
querystringiä, otsakkeita, evästeitä ja runkoa. Ulkopuoliselle jää näkyviin vain
palvelimen nimi ja liikenteen määrä. Selaimet vaativat HTTPS:ää nykyisin lähes
kaikelta, ja moni rajapinta (esim. sijaintitieto ja service workerit) toimii vain
salatulla yhteydellä. Käytä sovelluksissasi aina
https://-osoitteita.
HTTP/1.1, HTTP/2 ja HTTP/3
Edellä esitetty tekstimuotoinen pyyntö on HTTP/1.1:tä. Uudemmat versiot säilyttävät saman semantiikan — samat metodit, otsakkeet ja statuskoodit — mutta siirtävät ne eri tavalla:
- HTTP/1.1 on tekstimuotoinen. Yhdellä yhteydellä käsitellään yksi pyyntö kerrallaan, joten selain avaa useita rinnakkaisia yhteyksiä.
- HTTP/2 on binäärinen ja multipleksoitu: samassa yhteydessä kulkee useita pyyntöjä yhtä aikaa, ja otsakkeet pakataan.
- HTTP/3 toimii TCP:n sijasta QUIC-protokollan päällä UDP:n yli, jolloin yhden paketin katoaminen ei pysäytä muita samanaikaisia pyyntöjä.
Sovelluskehittäjän kannalta versio ei useinkaan näy: palvelin ja selain sopivat siitä keskenään, ja oma koodi käsittelee samoja metodeja ja otsakkeita versiosta riippumatta.
Metodit
Metodi kertoo, mitä resurssille halutaan tehdä. Kaksi käsitettä kannattaa tuntea:
- Turvallinen (safe) metodi ei muuta palvelimen tilaa. Selain, välimuisti tai hakukonerobotti saa toistaa sen vapaasti — ja toistaakin.
- Idempotentti metodi tuottaa saman lopputuloksen, tehtiinpä se kerran tai monta kertaa.
| Metodi | Käyttö | Turvallinen | Idempotentti |
|---|---|---|---|
GET |
Hae resurssi | kyllä | kyllä |
HEAD |
Kuten GET, mutta vain otsakkeet | kyllä | kyllä |
POST |
Lähetä tietoa käsiteltäväksi; luo uusi | ei | ei |
PUT |
Korvaa resurssi kokonaan | ei | kyllä |
PATCH |
Muuta osaa resurssista | ei | ei |
DELETE |
Poista resurssi | ei | kyllä |
OPTIONS |
Kysy, mitä resurssille voi tehdä | kyllä | kyllä |
HTML-lomake osaa vain GET- ja POST-metodit. Muut
ovat käytössä JavaScriptin fetch-kutsuissa ja
REST-rajapinnoissa.
Statuskoodit
Palvelin palauttaa jokaiseen pyyntöön statuskoodin, joka kertoo lyhyesti miten pyyntö onnistui. Ensimmäinen numero kertoo luokan:
- 1xx — pyyntö vastaanotettu, käsittely jatkuu
(
100 Continue,101 Switching Protocols,103 Early Hints) - 2xx — onnistui
- 3xx — uudelleenohjaus
- 4xx — asiakkaan virhe
- 5xx — palvelimen virhe
| Koodi | Nimi | Milloin oma sovellus palauttaa |
|---|---|---|
| 200 | OK | Normaali onnistunut vastaus. |
| 201 | Created | Uusi resurssi luotiin. Osoite kerrotaan
Location-otsakkeessa. |
| 204 | No Content | Onnistui, mutta vastauksessa ei ole runkoa. |
| 301 | Moved Permanently | Osoite vaihtui pysyvästi. Selaimet ja hakukoneet muistavat tämän pitkään, joten älä käytä kokeiluun. |
| 302 | Found | Väliaikainen ohjaus. Selaimet vaihtavat metodin GET:ksi, mikä ei ollut alkuperäinen tarkoitus — käytä mieluummin 303:a tai 307:ää. |
| 303 | See Other | Lomakkeen käsittelyn jälkeen. Kertoo selaimelle, että tulos haetaan GET-pyynnöllä toisesta osoitteesta. |
| 304 | Not Modified | Sisältö ei ole muuttunut; selain käyttää välimuistiaan. |
| 307 / 308 | Temporary / Permanent Redirect | Kuten 302 ja 301, mutta metodi ja runko säilyvät. |
| 400 | Bad Request | Pyyntö on virheellinen tai puutteellinen. |
| 401 | Unauthorized | Kirjautuminen puuttuu. Nimestään huolimatta kyse on tunnistautumisesta, ei käyttöoikeudesta. |
| 403 | Forbidden | Tunnistautuminen ei auta; oikeudet eivät riitä. Tämän saat myös, jos CGI-ohjelman tiedosto-oikeudet ovat väärin. |
| 404 | Not Found | Resurssia ei ole. |
| 405 | Method Not Allowed | Esim. POST osoitteeseen, joka hyväksyy vain GET-pyyntöjä. Flask
palauttaa tämän itse, jos methods-luettelo ei täsmää. |
| 422 | Unprocessable Content | Pyyntö on muodollisesti kelvollinen, mutta sisältö ei kelpaa (validointivirhe). |
| 429 | Too Many Requests | Pyyntöjä on tullut liikaa. |
| 500 | Internal Server Error | Sovellus kaatui. Tuttu näky, kun CGI-ohjelman rivinvaihdot tai oikeudet ovat väärin. |
| 502 / 503 / 504 | Bad Gateway / Service Unavailable / Gateway Timeout | Taustapalvelin ei vastaa, on ruuhkautunut tai hidas. |
Otsakkeet, jotka sovellus itse asettaa
Suurimman osan otsakkeista hoitaa www-palvelin. Nämä ovat niitä, jotka oma sovellus asettaa:
| Otsake | Käyttö |
|---|---|
Content-Type |
Mediatyyppi ja merkistö. Ilman tätä selain arvaa, ja arvaa usein väärin. |
Location |
Uudelleenohjauksen kohde. Kulkee aina 3xx-statuskoodin kanssa. |
Set-Cookie / Cookie |
Evästeen asettaminen ja palauttaminen. Käytä
HttpOnly- ja Secure-määreitä. |
Cache-Control |
Kuinka kauan vastausta saa säilyttää välimuistissa. Henkilökohtaiselle
sisällölle private tai no-store. |
Content-Disposition |
Näytetäänkö sisältö selaimessa vai tarjotaanko sitä tallennettavaksi, ja millä tiedostonimellä. |
from flask import Response, redirect, url_for
# mediatyyppi ja merkistö
vastaus = Response(sisalto, content_type="application/xhtml+xml; charset=UTF-8")
# oma otsake
vastaus.headers["Cache-Control"] = "no-store"
# uudelleenohjaus: asettaa Location-otsakkeen ja statuskoodin
return redirect(url_for("lauta"), code=303)
Lomake
form-elementti määrittelee lomakkeen alkamis- ja loppumiskohdan.
Sen sisään sijoitetaan lomake-elementit, ja sen attribuutit kertovat, minne ja
miten tiedot lähetetään.
<form action="https://palvelin.example/sovellus" method="post">
<p>
<label for="nimi">Nimi:</label>
<input id="nimi" type="text" name="nimi" />
</p>
<p>
<label for="email">Sähköposti:</label>
<input id="email" type="email" name="email" />
</p>
<p>
<label for="kommentti">Kommentti:</label>
<textarea id="kommentti" name="kommentti" rows="4" cols="40"></textarea>
</p>
<p>
<input type="submit" name="laheta" value="Lähetä kommenttisi" />
</p>
</form>
Jokaisella kentällä on oltava name, koska sovellus lukee arvot
juuri sillä nimellä. label-elementin for-attribuutti
viittaa kentän id-attribuuttiin — se ei ole sama asia kuin
name. Älä käytä value-attribuuttia ohjetekstinä; siihen
on placeholder, ja varsinainen selitys kuuluu labeliin.
action-attribuutti
action kertoo osoitteen, jossa lomakkeen käsittelevä ohjelma
sijaitsee. Tyhjä arvo (action="") lähettää lomakkeen samaan
osoitteeseen, jossa lomake itse on — usein juuri se, mitä halutaan.
method-attribuutti
method kertoo, miten tiedot toimitetaan. HTML-lomakkeen
mahdolliset arvot ovat get ja post, ja ne viittaavat
suoraan HTTP-metodeihin.
GET siirtää tiedot osoitteen querystringissä:
https://palvelin.example/sovellus?nimi=Etunimi+Sukunimi&email=maija%40example.com&kommentti=t%C3%A4h%C3%A4n+kommentti&laheta=L%C3%A4het%C3%A4+kommenttisi
Ohjelman osoite ja tiedot erottaa kysymysmerkki, kentät erottaa
&-merkki ja kentän nimen sen arvosta yhtäsuuruusmerkki.
Erikoismerkit on koodattu, ja välilyönnin paikalla on + tai
%20.
POST siirtää tiedot pyynnön rungossa, jolloin ne eivät näy osoitteessa:
POST /sovellus HTTP/1.1
Host: palvelin.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 96
nimi=Etunimi+Sukunimi&email=maija%40example.com&kommentti=t%C3%A4h%C3%A4n+kommentti&laheta=L%C3%A4het%C3%A4
POST ei ole turvallisuusominaisuus. Tiedot eivät näy osoiterivillä, selainhistoriassa, kirjanmerkeissä eivätkä palvelinlokeissa, mutta ne näkyvät sellaisenaan kehittäjätyökalujen verkkovälilehdellä ja kenelle tahansa, joka pääsee liikennettä katsomaan. Ainoa asia, joka salaa tiedot matkalla, on HTTPS.
Kumpi siis milloinkin?
- GET: haut ja muut tilaa muuttamattomat pyynnöt, joista halutaan voida tehdä kirjanmerkki ja jotka saa turvallisesti ladata uudelleen. Käytännön rajana on osoitteen enimmäispituus, noin 2000 merkkiä.
- POST: lisäykset, muutokset, poistot ja suuret tietomäärät.
enctype-attribuutti
enctype kertoo, missä muodossa POST-lomakkeen runko koodataan.
GET-lomakkeeseen se ei vaikuta.
application/x-www-form-urlencoded(oletus) — sama muoto kuin querystringissämultipart/form-data— pakollinen, jos lomakkeella lähetetään tiedostoja (<input type="file" />)text/plain— vain testikäyttöön, ei koodaa erikoismerkkejä yksikäsitteisesti
Merkistöt ja percent-encoding
Osoitteessa saa esiintyä vain rajattu joukko merkkejä. Kaikki muut — ääkköset, välilyönnit, lainausmerkit, hakasulkeet — koodataan percent-encoding-tavalla prosenttimerkillä ja merkin tavujen heksadesimaaliesityksellä.
Koodauksen tulos riippuu siitä, millä merkistöllä teksti ensin muutetaan tavuiksi. UTF-8:ssa ä on kaksi tavua, ISO-8859-1:ssä yksi:
from urllib.parse import quote_plus, unquote_plus
quote_plus("Sähköpostiosoite")
# 'S%C3%A4hk%C3%B6postiosoite' UTF-8, nykyinen ja oikea muoto
quote_plus("Sähköpostiosoite", encoding="latin-1")
# 'S%E4hk%F6postiosoite' ISO-8859-1, vanha muoto
unquote_plus("S%E4hk%F6postiosoite")
# 'S\ufffdhk\ufffdpostiosoite' UTF-8:na luettuna tavut ovat kelvottomia
Vanhoissa materiaaleissa (myös tämän kurssin aiemmissa versioissa) näkee osoitteita seuraavaan tapaan:
http://palvelin.example/sovellus?Nimi=Etunimi+Sukunimi&email=Sahk%F6postiosoite&kommentti=t%E4h%E4n+kommentti&laheta=L%E4het%E4+kommenttisi
Tässä %F6 on ISO-8859-1:n ö ja %E4 sen ä. Nykyinen
selain lähettäisi samat merkit muodossa %C3%B6 ja
%C3%A4. Esimerkki kannattaa tunnistaa, koska vastaavia osoitteita
on yhä liikkeellä, mutta älä käytä sitä mallina: jos sovellus
purkaa tavut väärällä merkistöllä, tuloksena on joko korvausmerkkejä
(S�hk�posti) tai mojibakea
(Sähköposti).
Käytännössä koodaus menee oikein, kun kaikki lenkit ketjussa ovat UTF-8:aa:
dokumentin Content-Type, meta charset ja tarvittaessa
lomakkeen accept-charset-attribuutti. Selain koodaa lomakkeen sillä
merkistöllä, jolla lomakesivu on tarjoiltu.
Osoitteita ei kannata rakentaa käsin merkkijonoja liimaamalla, vaan koodausfunktioilla:
from urllib.parse import urlencode
from flask import url_for
import html
kysely = urlencode({"nimi": "Mälli Henkilö", "kurssi": "Ällö kurssi"})
osoite = url_for("lauta", l=8, p1="Mälli Henkilö")
# html-dokumenttiin sijoitettaessa myös &-merkit on koodattava
linkki = '<a href="' + html.escape(osoite) + '">Linkki</a>'
Lomakkeen käsittely
- Selain pyytää palvelimelta lomakesivun.
- Palvelin vastaa otsakkeilla ja lomakkeen sisältävällä dokumentilla.
- Käyttäjä täyttää lomakkeen ja lähettää sen. Selain kokoaa kenttien nimet ja
arvot ja lähettää ne
action-osoitteeseenmethod-attribuutin kertomalla metodilla. - Palvelimella suoritettava ohjelma käsittelee tiedot.
- Ohjelma palauttaa tuloksen: virheilmoitukset ja korjattavaksi palautetun lomakkeen, tulossivun, uudelleenohjauksen tai vaikka kuvan.
Se, miten ohjelma saa tiedot käyttöönsä, riippuu rajapinnasta. Suoraan
CGI-rajapintaa
käytettäessä pyynnön tiedot tulevat ympäristömuuttujissa
(REQUEST_METHOD, QUERY_STRING,
CONTENT_LENGTH, HTTP_*) ja POST-runko standardisyötteestä
luettuna. Flask tarjoaa saman tiedon valmiiksi jäsennettynä:
from flask import request
request.method # "GET" tai "POST"
request.args.get("nimi") # osoitteen parametrit
request.form.get("nimi") # POST-lomakkeen kentät
request.values.get("nimi") # molemmat yhdessä
request.files.get("liite") # multipart/form-data -lähetykset
request.headers.get("User-Agent") # pyynnön otsakkeet
request.environ # koko ympäristö, vrt. CGI-muuttujat
POST, uudelleenohjaus ja GET
Jos POST-pyyntöön vastataan suoraan sivulla, selaimen osoitteessa on yhä käsittelijän osoite ja pyyntönä POST. Kun käyttäjä painaa päivitystä tai selaa taaksepäin, selain kysyy lomakkeen lähettämistä uudelleen — ja sama lisäys tai poisto tehdään toistamiseen.
Ratkaisu on POST/Redirect/GET: käsittelijä tekee muutoksen ja vastaa uudelleenohjauksella tulossivulle, jonka selain hakee GET-pyynnöllä. Silloin osoiterivillä on tulossivun osoite, sivun voi päivittää vapaasti ja siitä voi tehdä kirjanmerkin.
@app.route("/kommentti", methods=["POST"])
def tallenna():
tallenna_kommentti(request.form.get("kommentti", ""))
# 303 kertoo, että tulos haetaan GET-pyynnöllä toisesta osoitteesta
return redirect(url_for("kiitos"), code=303)
HTTP on tilaton
HTTP on oletuksena tilaton. Palvelin ei tiedä pyyntöjen välillä mitään edellisistä pyynnöistä: jokainen pyyntö on itsenäinen. Tila on siis kuljetettava jotenkin mukana:
- Osoitteessa — tila koodataan querystringiin ja sivun linkkeihin. Toimii ilman evästeitä, mutta osoitteet kasvavat ja tila on käyttäjän muokattavissa. Tätä harjoitellaan viikkotehtävissä.
- Dokumentin rakenteessa — tila piilokentissä, jolloin se kulkee POST-lomakkeen mukana.
- Evästeissä — palvelin lähettää
Set-Cookie-otsakkeen, ja selain palauttaa arvonCookie-otsakkeessa jokaisessa seuraavassa pyynnössä. - Sessiossa — evästeessä kulkee vain tunniste, ja varsinainen
tila on palvelimella tai allekirjoitetussa evästeessä (Flaskin
session).
Palvelinprosessin muistiin tilaa ei voi jättää: CGI-ohjelmassa tulkki käynnistyy joka pyynnöllä uudelleen, ja pilvipalvelussa samasta sovelluksesta voi olla käynnissä useita instansseja.
Esimerkit
- Lomake
- Lomakkeen käsittelijä (lähdekoodi)
- Ympäristömuuttujat
tulostava malliohjelma
#!/usr/bin/python3 import os print("Content-type: text/plain; charset=UTF-8\n") for x in os.environ.keys(): print(x + "\t" + os.environ[x])
Lisätietoa
- MDN: HTTP — paras hakuteos yksittäisille otsakkeille ja statuskoodeille
- RFC 9110: HTTP Semantics (korvaa vanhan RFC 2616:n)
- RFC 9111: HTTP Caching
- RFC 9112: HTTP/1.1, RFC 9113: HTTP/2, RFC 9114: HTTP/3
- HTML-standardi: lomakkeen lähetys
- WSGI, CGI ja Flask
- www-lomakkeet (ITKP1011), Lomakkeet (TIEA2120)
Käyttäjien kommentit