Elk onderdeel van een URL en wat het mag bevatten

Elk onderdeel van een URL benoemd aan de hand van een uitgewerkt voorbeeld, welke tekens waar percent-encoded moeten worden, hoe query strings zich echt gedragen, en hoe je een link herkent die liegt over zijn host.

Een URL is één string die zegt welk protocol er gesproken moet worden, tegen welke machine, en wat er gevraagd wordt. Er een goed lezen is vooral een kwestie van weten waar elk onderdeel ophoudt, want de scheidingstekens zijn losse tekens en het eerste exemplaar wint.

https://alex:s3cret@www.example.co.uk:8443/docs/intro?lang=en&page=2#notes
│       │           │                 │   │           │              └─ fragment
│       │           │                 │   │           └──────────────── query
│       │           │                 │   └──────────────────────────── path
│       │           │                 └──────────────────────────────── port
│       │           └────────────────────────────────────────────────── host
│       └────────────────────────────────────────────────────────────── userinfo
└────────────────────────────────────────────────────────────────────── scheme

De onderdelen

OnderdeelBegrensd doorWaarvoor het dientBereikt de server
Schemeeindigt bij de eerste :Welk protocol en welke regels voor de rest geldenBepaalt de verbinding
Userinfona //, eindigt bij @Inloggegevens, al lang afgeraden voor http en httpsAlleen als de client ze meestuurt
Hosteindigt bij :, /, ? of #Een domeinnaam of IP-adres om op te zoeken en verbinding mee te makenJa, in de Host header
Poortna :, eindigt bij /, ? of #De TCP-poort, standaard 80 voor http en 443 voor httpsWordt gebruikt voor de verbinding
Padbegint bij /, eindigt bij ? of #Welke resource op die hostJa, in de request line
Querybegint bij ?, eindigt bij #Parameters voor die resourceJa, in de request line
Fragmentbegint bij #, loopt tot het eindeEen plek binnen de resourceNee

Host en poort vormen samen de authority. Het fragment is de vreemde eend: het wordt weggehaald voordat het request wordt opgebouwd, dus een server ziet het nooit. Browsers gebruiken het voor ankers en client-side routing, en sommige authenticatieflows geven tokens bewust terug in een fragment zodat die tokens nooit in een serverlog terechtkomen. Het komt nog steeds in de browsergeschiedenis, dus het is niet privé, alleen niet verstuurd.

Gereserveerde en niet-gereserveerde tekens

Percent-encoding schrijft een byte als % gevolgd door twee hexcijfers, op basis van de UTF-8 bytes van het teken. é wordt %C3%A9, twee bytes en twee escapes.

Vier tekens zijn niet-gereserveerd naast letters en cijfers, en hoeven nergens gecodeerd te worden: - . _ ~. Al het andere is ofwel gereserveerd, wat betekent dat het ergens in een URL een structurele taak heeft, of het moet gecodeerd worden. De gereserveerde verzameling is : / ? # [ ] @ en ! $ & ' ( ) * + , ; =.

Het woord "gereserveerd" betekent niet "altijd verboden". Het betekent dat het teken in een bepaald onderdeel een scheidingsteken is, en dat het alleen gecodeerd hoeft te worden in de onderdelen waar het als zodanig gelezen zou worden.

TekenIn een padsegmentIn een querywaarde
Spatie%20, altijd%20 of +
/%2F, anders splitst het segmentMag zoals het is
?%3F, anders houdt het pad daar opMag zoals het is
#%23, altijd%23, altijd
& en =Mogen zoals ze zijn%26 en %3D, anders valt het paar uiteen
+Mag, en betekent een plusteken%2B, anders wordt het mogelijk als spatie gedecodeerd

Dat is de praktische regel: codeer het teken dat het onderdeel waarin je zit zou beëindigen. Een spatie in een pad moet %20 zijn omdat een letterlijke spatie de URL in de meeste parsers beëindigt, terwijl een spatie in een querywaarde %20 of het oudere + mag zijn. Een URL Encoder is de betrouwbare manier om dit te doen, omdat een hele URL coderen en één component coderen verschillende operaties zijn en de tweede bijna altijd is wat je wilt.

Query strings zijn een conventie

De specificatie zegt alleen dat de query alles is tussen ? en #, opgebouwd uit de toegestane tekens. De vorm key=value&key=value komt uit HTML form encoding, niet uit de URI-standaard. Niets houdt een server tegen om ?a:1;b:2 te parsen zoals hij wil.

Omdat de vorm conventie is, hebben herhaalde sleutels geen vastgelegde betekenis, en elke stack koos een eigen antwoord:

Gedrag bij ?id=1&id=2Waar
Beide waarden, als lijstPython parse_qs, Node querystring, Express
Alleen de eerste waardeGo Query().Get, Java getParameter
Alleen de laatste waardePHP, Rails
Samengevoegd tot 1,2ASP.NET request collections

Arrays erven hetzelfde probleem. ids=1&ids=2, ids[]=1&ids[]=2, ids[0]=1&ids[1]=2 en ids=1,2 worden allemaal veel gebruikt, en alleen de server bepaalt welke vorm hij begrijpt. Bracketnotatie moet gecodeerd worden om strikt geldig te zijn (ids%5B%5D=1), al accepteren de meeste servers de letterlijke blokhaken. Kies de vorm die de ontvangende kant documenteert en houd die consequent aan.

Het plusteken verdient een eigen waarschuwing. In application/x-www-form-urlencoded data, een form-encoded request body, betekent + een spatie. In een URL-pad betekent het een letterlijke plus. In een query string hangt het van de parser af, en de meeste webframeworks passen daar form decoding toe, waardoor + stilzwijgend een spatie wordt. Elke waarde die terecht een plus kan bevatten, zoals standaard Base64-uitvoer of een telefoonnummer, moet als %2B verstuurd worden.

Relatieve verwijzingen

Een relatieve verwijzing wordt tegen een basis-URL opgelost door alles na de laatste / van de basis te vervangen. Die laatste slash is de hele regel, en daarom doet een afsluitende slash ertoe.

BasisVerwijzingResultaat
https://ex.com/docs/introguidehttps://ex.com/docs/guide
https://ex.com/docs/intro/guidehttps://ex.com/docs/intro/guide
https://ex.com/docs/intro/guidehttps://ex.com/guide
https://ex.com/docs/intro/../guidehttps://ex.com/docs/guide
https://ex.com/docs/intro?a=1?b=2https://ex.com/docs/intro?b=2
https://ex.com/docs/intro?a=1#tophttps://ex.com/docs/intro?a=1#top
https://ex.com/docs/intro//cdn.ex.com/x.jshttps://cdn.ex.com/x.js

De laatste rij is een scheme-relatieve verwijzing: twee slashes aan het begin behouden het scheme van de basis en vervangen de authority. Een verwijzing die met ? begint behoudt het pad en laat de oude query vallen, en een die met # begint behoudt beide.

Wanneer twee URL's dezelfde resource zijn

Scheme en host zijn hoofdletterongevoelig, dus HTTPS://Example.COM en https://example.com zijn één adres. Alles na de host is hoofdlettergevoelig wat de standaard betreft, ook al kiest een bepaalde server ervoor hoofdletters te negeren.

Deze paren zijn gelijkwaardig:

  • https://example.com:443/a en https://example.com/a, omdat de poort de standaardpoort is
  • https://example.com en https://example.com/, omdat een leeg pad de root betekent
  • /a/%7Euser en /a/~user, omdat ~ niet-gereserveerd is en percent-encoding er niets aan verandert

Deze paren zijn dat niet:

  • /docs en /docs/, die verschillende resources zijn, al leiden de meeste servers de een door naar de ander
  • /p?a=1&b=2 en /p?b=2&a=1, omdat de volgorde van parameters deel uitmaakt van de string
  • /index.html en /, tenzij de server iets anders zegt

Caches en CDN's gebruiken de exacte bytes als sleutel, dus een toegevoegde trackingparameter of een herschikte query splitst één gecacht object in twee en halveert de hit rate. Zoekmachines behandelen de varianten als dubbele pagina's, tenzij een canonical link naar één gekozen vorm wijst. De oplossing is één vorm kiezen, de rest daarheen doorverwijzen met een 301, en parameters die het antwoord niet veranderen weglaten.

Lengte, logs en privacy

De specificatie kent geen lengtelimiet, maar overal daarbuiten bestaan die wel. Gangbare serverinstellingen begrenzen de hele request line op ongeveer 8 KB, sommige proxies en appliances op 4 KB, en oudere clients op zo'n 2 KB. Alles wat e-mailclients, QR-codes en linkverkorters moet overleven, blijft het veiligst onder ongeveer 2000 tekens.

Het sterkere argument om URL's kort te houden is dat een query string niet privé is. Hij wordt in access logs geschreven, in de Referer header aan derden doorgegeven, in de browsergeschiedenis bewaard, met bladwijzers opgeslagen, en gekopieerd zodra iemand de link deelt. Sessietokens, wachtwoordherstelcodes, API-sleutels en persoonsgegevens horen daar dus niet thuis. Data die groot of gevoelig is gaat in een request body, die geen praktische bovengrens heeft en standaard niet gelogd wordt.

De authority begint na :// en eindigt bij de allereerste /, ? of #. Lees dat stuk, en lees dan de laatste twee of drie labels ervan. Dat is de echte host, en niets links daarvan verandert daar iets aan.

https://www.paypal.com@198.51.100.7/secure/login
        ^^^^^^^^^^^^^^ ^^^^^^^^^^^^
        userinfo       real host

Veelgebruikte verhulling om te herkennen:

  • Tekst vóór een @ is userinfo, nooit de host, en dat kan elke merknaam zijn.
  • paypal.com.secure-login.example is een subdomein van secure-login.example. Labels lees je van rechts naar links.
  • https://short.example/https://www.bank.com/login zet een hele overtuigende URL in het pad.
  • Een host die met xn-- begint is Punycode, de ASCII-vorm van een geïnternationaliseerde domeinnaam die door IDNA wordt geproduceerd. münchen.de is op de lijn xn--mnchen-3ya.de, wat legitiem is, maar hetzelfde mechanisme maakt homografen mogelijk: de Cyrillische а (U+0430) is visueel identiek aan de Latijnse a, en een domein dat hem gebruikt codeert naar zoiets als xn--pple-43d.com. Browsers tonen de Punycode-vorm wanneer een label schriftsystemen mengt, al verschillen de regels en bieden ze geen garantie.
  • Gecodeerde scheidingstekens zoals %2F of %40 binnen een host zijn een bewuste poging om een parser te misleiden.

De link in een URL Parser plakken beslist het in één stap, want een echte parser past dezelfde regel toe waarbij het eerste scheidingsteken wint en toont de host apart, precies zoals de browser dat doet. Om veel links tegelijk te controleren is een Regex Tester een snelle manier om vast te stellen welke ervan echt de authority hebben die je verwacht.