Elk onderdeel van een URL en wat het mag bevatten
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
| Onderdeel | Begrensd door | Waarvoor het dient | Bereikt de server |
|---|---|---|---|
| Scheme | eindigt bij de eerste : | Welk protocol en welke regels voor de rest gelden | Bepaalt de verbinding |
| Userinfo | na //, eindigt bij @ | Inloggegevens, al lang afgeraden voor http en https | Alleen als de client ze meestuurt |
| Host | eindigt bij :, /, ? of # | Een domeinnaam of IP-adres om op te zoeken en verbinding mee te maken | Ja, in de Host header |
| Poort | na :, eindigt bij /, ? of # | De TCP-poort, standaard 80 voor http en 443 voor https | Wordt gebruikt voor de verbinding |
| Pad | begint bij /, eindigt bij ? of # | Welke resource op die host | Ja, in de request line |
| Query | begint bij ?, eindigt bij # | Parameters voor die resource | Ja, in de request line |
| Fragment | begint bij #, loopt tot het einde | Een plek binnen de resource | Nee |
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.
| Teken | In een padsegment | In een querywaarde |
|---|---|---|
| Spatie | %20, altijd | %20 of + |
/ | %2F, anders splitst het segment | Mag zoals het is |
? | %3F, anders houdt het pad daar op | Mag 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=2 | Waar |
|---|---|
| Beide waarden, als lijst | Python parse_qs, Node querystring, Express |
| Alleen de eerste waarde | Go Query().Get, Java getParameter |
| Alleen de laatste waarde | PHP, Rails |
Samengevoegd tot 1,2 | ASP.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.
| Basis | Verwijzing | Resultaat |
|---|---|---|
https://ex.com/docs/intro | guide | https://ex.com/docs/guide |
https://ex.com/docs/intro/ | guide | https://ex.com/docs/intro/guide |
https://ex.com/docs/intro | /guide | https://ex.com/guide |
https://ex.com/docs/intro/ | ../guide | https://ex.com/docs/guide |
https://ex.com/docs/intro?a=1 | ?b=2 | https://ex.com/docs/intro?b=2 |
https://ex.com/docs/intro?a=1 | #top | https://ex.com/docs/intro?a=1#top |
https://ex.com/docs/intro | //cdn.ex.com/x.js | https://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/aenhttps://example.com/a, omdat de poort de standaardpoort ishttps://example.comenhttps://example.com/, omdat een leeg pad de root betekent/a/%7Euseren/a/~user, omdat~niet-gereserveerd is en percent-encoding er niets aan verandert
Deze paren zijn dat niet:
/docsen/docs/, die verschillende resources zijn, al leiden de meeste servers de een door naar de ander/p?a=1&b=2en/p?b=2&a=1, omdat de volgorde van parameters deel uitmaakt van de string/index.htmlen/, 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.
Een link lezen voordat je erop klikt
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.exampleis een subdomein vansecure-login.example. Labels lees je van rechts naar links.https://short.example/https://www.bank.com/loginzet 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.deis op de lijnxn--mnchen-3ya.de, wat legitiem is, maar hetzelfde mechanisme maakt homografen mogelijk: de Cyrillischeа(U+0430) is visueel identiek aan de Latijnsea, en een domein dat hem gebruikt codeert naar zoiets alsxn--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
%2Fof%40binnen 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.