Webentwicklung

HTTP-Range-Requests für Dateidownloads - Partial Content ist ein Protokoll, kein String-Ausschnitt

HTTP-Range-Requests für Dateidownloads - Partial Content ist ein Protokoll, kein String-Ausschnitt

Ein großer Download bricht auf halber Strecke ab. Die lokale Datei ist noch vorhanden, daher erscheint es naheliegend, nur die fehlenden Bytes anzufordern. Doch was geschieht, wenn sich die Datei hinter der URL während der Unterbrechung geändert hat? Wird die zweite Hälfte der neuen Datei an die erste Hälfte der alten angehängt, entsteht ein Ergebnis, das zu keiner der beiden Versionen gehört.

So lassen sich HTTP Range Requests sinnvoll verstehen: nicht als Abkürzung zum Zerschneiden einer Datei, sondern als kleines Protokoll zur Übertragung eines Teils einer bestimmten Repräsentation. Die Berechnung ist wichtig, doch die Identität der Repräsentation, der Response-Status und Validatoren sind ebenso entscheidend.

Dieser Artikel folgt RFC 9110, der aktuellen Spezifikation der HTTP-Semantik. Er konzentriert sich auf Downloads über GET. Das Fortsetzen von Uploads, partielles PUT, Object-Storage-APIs und anbieterspezifisches CDN-Verhalten sind eigenständige Themen.

Ein Range bezieht sich auf eine Repräsentation, nicht auf eine abstrakte Datei

Eine URL identifiziert eine Ressource, während eine HTTP-Response eine ausgewählte Repräsentation dieser Ressource überträgt. Der Unterschied wird sichtbar, sobald Content Negotiation oder Content Coding beteiligt sind. Ein Server kann für einen Request eine bestimmte Sprache, einen Medientyp oder eine codierte Form auswählen. Ein Byte Range bezieht sich dann auf die Oktettfolge dieser ausgewählten Repräsentation.

RFC 9110 definiert die Range-Verarbeitung für GET. Ein Server darf einen Range-Header ignorieren. Der Header ist daher eine Anfrage und keine Anweisung, die ein Client erzwingen kann. Unterstützung ist dennoch nützlich, um unterbrochene Übertragungen fortzusetzen oder nur einen Teil einer großen Repräsentation abzurufen.

Die übliche Einheit ist bytes. Die Offsets beginnen bei null, und beide Grenzen sind inklusive:

Range: bytes=0-999
Range: bytes=1000-
Range: bytes=-500

Die erste Form fordert 1.000 Bytes an, nicht 999. Die zweite beginnt bei Byte-Offset 1.000 und reicht bis zum Ende. Die dritte fordert bis zu 500 Bytes am Ende an. Ist ein Suffix länger als die Repräsentation, wird die gesamte Repräsentation zum ausgewählten Bereich. Reicht eine explizite Endposition über die aktuelle Länge hinaus, interpretiert der Server sie als den verbleibenden Teil, statt über das Ende hinaus zu lesen.

Es gibt eine weitere, weniger offensichtliche Grenze. Wenn ein Content Coding wie gzip angewendet wurde, beziehen sich die Offsets auf die codierte Bytefolge und nicht auf die Bytes nach dem Decodieren. Ein Client sollte deshalb die Metadaten der Response als maßgeblich behandeln, statt Offsets anhand einer anders codierten Kopie zu berechnen.

200, 206 und 416 erzählen unterschiedliche Geschichten

Drei Statuscodes bilden den praktischen Entscheidungsbaum. Sie sind nicht austauschbar:

  • 200 OK bedeutet, dass die Response die vollständige ausgewählte Repräsentation enthält. Ein Server kann sie zurückgeben, weil er den Range ignoriert, für diese Ressource keine Ranges unterstützt oder eine vollständige Response für angemessen hält.
  • 206 Partial Content bedeutet, dass der Server die Anfrage mit einem oder mehreren Teilen der ausgewählten Repräsentation erfüllt.
  • 416 Range Not Satisfiable bedeutet, dass die Range-Menge abgelehnt wurde, weil keiner der angeforderten Bereiche erfüllbar war oder weil eine übermäßige Menge kleiner oder überlappender Bereiche als missbräuchlich behandelt wurde.

Bei einer einteiligen 206-Response ist ein Content-Range erforderlich, der den Body innerhalb der vollständigen Repräsentation einordnet. Eine Anfrage nach den ersten 1.000 Bytes einer 10.000 Bytes langen Repräsentation kann Folgendes ergeben:

HTTP/1.1 206 Partial Content
Content-Length: 1000
Content-Range: bytes 0-999/10000

[1000 response-body bytes]

Content-Length beschreibt hier den Body dieser Nachricht. Der Wert behauptet nicht, dass die vollständige Repräsentation nur 1.000 Bytes lang ist. Der Nenner in Content-Range gibt die vollständige Länge an.

Fordert ein Client bytes=12000- von dieser 10.000 Bytes langen Repräsentation an, überlappt die erste Position die Repräsentation nicht. RFC 9110 besagt, dass ein Server bei einer 416-Response auf eine Byte-Range-Anfrage die aktuelle Gesamtlänge angeben sollte:

HTTP/1.1 416 Range Not Satisfiable
Content-Range: bytes */10000

Das Wort sollte ist wichtig. Ein Server darf Range weiterhin ignorieren und eine vollständige 200-Response senden. Ein Download-Client muss Status und Response-Header prüfen, bevor er entscheidet, ob der empfangene Body angehängt, ersetzt oder verworfen wird. Jede Response blind an eine partielle lokale Datei anzuhängen, ist ein Korruptionsfehler, der nur auf die passende Unterbrechung wartet.

Accept-Ranges kündigt an, verspricht aber nichts

Eine Response kann Accept-Ranges: bytes enthalten, um Byte-Range-Unterstützung für die Zielressource anzukündigen. Mit Accept-Ranges: none kann sie darauf hinweisen, dass keine Range-Einheit unterstützt wird. Das Feld hilft Clients bei der Entscheidung, ob sie eine Fortsetzen-Funktion anbieten sollen.

Es ist jedoch kein dauerhaftes Versprechen. Die Spezifikation warnt ausdrücklich davor anzunehmen, dass eine künftige Anfrage eine partielle Response erhält, nur weil eine frühere Response Unterstützung angekündigt hat. Repräsentation, Serverzustand oder Intermediary können sich ändern. Umgekehrt darf ein Client einen Range versuchen, auch wenn er noch kein Accept-Ranges gesehen hat.

Ein HEAD-Request ist eine praktische Möglichkeit, Metadaten zu prüfen, ohne den normalen Response-Body zu übertragen. Der MDN-Leitfaden zu Range Requests demonstriert diesen Ansatz. Was tatsächlich geschieht, entscheidet jedoch weiterhin die anschließende GET-Response.

Sicheres Fortsetzen erfordert die Identität der Repräsentation

Angenommen, ein Client hat die Bytes 0 bis 4.999 gespeichert und benötigt den Rest. Range: bytes=5000- beantwortet nur die Frage nach dem Offset. Es beweist nicht, dass die Ressource noch dieselbe Repräsentation besitzt.

If-Range verbindet beide Fragen. Mit einem zuvor vom Server empfangenen starken Entity Tag kann ein Request so aussehen:

GET /downloads/archive.tar HTTP/1.1
Host: example.test
Range: bytes=5000-
If-Range: "release-42"

Stimmt der Validator noch überein, kann der Server den Range verarbeiten und 206 zurückgeben. Stimmt er nicht überein, ignoriert der Server den Range und sendet die aktuelle Repräsentation vollständig, normalerweise als 200. Dieser Fallback ist beabsichtigt: Der Client kann seine veraltete partielle Kopie ersetzen, statt Bytes verschiedener Versionen zu kombinieren.

Ein schwacher Entity Tag, erkennbar am Präfix W/, darf nicht in If-Range gesendet werden. Ein HTTP-Datum ist ebenfalls nur unter den Strong-Validator-Bedingungen der Spezifikation und nur dann zulässig, wenn der Client keinen Entity Tag besitzt. In der Praxis ist ein starker ETag die klarere Wahl, wenn die Anwendung ihn bei jeder beobachtbaren Änderung der Repräsentationsdaten ändern kann.

Dadurch verschwindet nicht jedes Race beim Fortsetzen automatisch. Der Server muss eine Repräsentation konsistent auswählen und übertragen, und der Validator muss diese Repräsentation tatsächlich beschreiben. Unterschiedlich codierte Formen benötigen außerdem unterschiedliche starke Tags, wenn sich ihre Repräsentationsdaten unterscheiden.

Mehrere Ranges schaffen eine andere Stufe der Komplexität

Ein Client kann mehr als ein Intervall in einem Header anfordern. Eine erfolgreiche Response verwendet dann multipart/byteranges mit einer Boundary und einem separaten Content-Range in jedem Body-Teil. Es handelt sich nicht um einen einzelnen Byte-Ausschnitt, aus dem einige Lücken entfernt wurden.

Die Funktion hat legitime Anwendungsfälle, erhöht aber auch den Aufwand für Parsing und Response-Erzeugung. RFC 9110 erlaubt einem Server, übermäßige Mengen zu ignorieren, zusammenzufassen oder abzulehnen, etwa viele kleine Ranges oder mehr als zwei überlappende Ranges. Der Sicherheitsabschnitt weist darauf hin, dass ein kleiner Request sonst unverhältnismäßige Kosten für Speicher, Bandbreite und Verarbeitung verursachen kann.

Für einen kleinen Download-Endpunkt, der nur Pause und Fortsetzen benötigt, ist die Unterstützung eines einzelnen Range oft eine vernünftige technische Grenze. Das ist eine Designentscheidung und keine HTTP-Anforderung. NGINX bietet die Direktive max_ranges: Laut der offiziellen Dokumentation des Core-Moduls werden Requests oberhalb der konfigurierten Anzahl so verarbeitet, als wären keine Byte Ranges angegeben; der Wert null deaktiviert Byte Ranges. Das passende Limit hängt von der Anwendung ab. Es gibt keine universelle Zahl, die ohne Verständnis der Clients übernommen werden sollte.

Die Anwendung autorisiert, der Dateiserver überträgt

Öffentliche statische Dateien können normalerweise direkt von einem fähigen Webserver ausgeliefert werden. Private Downloads ergänzen eine Autorisierungsentscheidung: Die Anwendung muss feststellen, ob dieser Benutzer auf diese Ressource zugreifen darf. Das bedeutet nicht zwangsläufig, dass PHP jeden Range parsen und jedes Byte streamen sollte.

Ein praktisches NGINX-Muster ist eine internal-Location. Die Anwendung authentifiziert den Request, löst eine opake Ressourcenkennung in einen vertrauenswürdigen internen Pfad auf und sendet bei erlaubtem Zugriff den Response-Header X-Accel-Redirect. NGINX führt anschließend einen internen Request aus und liefert die Datei. Die Core-Dokumentation nennt X-Accel-Redirect als Quelle interner Requests; ein externer Request kann eine internal-Location nicht direkt abrufen.

Diese Aufteilung belässt die Autorisierung im Anwendungscode und delegiert die Details der Dateiübertragung an den Server. Sie ersetzt keine sichere Pfadbehandlung. Ein vom Benutzer kontrollierter Dateiname darf nicht ungeprüft zu einem Dateisystempfad oder einer internen URI werden, und jeder neue Download-Request, einschließlich eines fortgesetzten Requests, benötigt weiterhin die erforderliche Autorisierung.

Eine eigene PHP-Implementierung kann gerechtfertigt sein, wenn Anforderungen an Speicherung oder Transformation eine Delegation verhindern. Ein produktiver Parser muss jedoch große Dezimalwerte ohne Integer Overflow, ungültige Syntax, offene und Suffix-Ranges, Conditional Requests, Response-Metadaten, mehrere Ranges oder eine ausdrückliche Richtlinie dagegen, Streaming-Fehler und das Autorisierungsmodell behandeln. Ein kurzes Snippet, das nur bytes=start-end abdeckt, würde mehr Risiken verbergen als erklären.

Das Verhalten testen, nicht nur einen Header

Die folgenden Befehle bilden eine kompakte Prüfreihe. Die URL sollte durch eine nicht sensible Testressource ersetzt werden:

curl -I 'https://example.test/downloads/sample.bin'

curl -sS -D - -o /dev/null \
  -H 'Range: bytes=0-999' \
  'https://example.test/downloads/sample.bin'

curl -sS -D - -o /dev/null \
  -H 'Range: bytes=999999999-' \
  'https://example.test/downloads/sample.bin'

Status, Content-Range, Content-Length, ETag und Content Coding sollten gemeinsam geprüft werden. Danach folgen Tests mit passendem und nicht passendem If-Range, fehlgeschlagener Autorisierung, einer leeren Repräsentation sowie jedem in Produktion verwendeten Proxy- oder CDN-Pfad. Ein Ergebnis direkt am Origin belegt nicht automatisch das Verhalten eines Intermediary.

Für eine tatsächlich unterbrochene Übertragung beschreibt die offizielle everything curl-Dokumentation --continue-at -. Damit wird der Resume-Offset aus der bereits vorhandenen Zieldatei abgeleitet:

curl --continue-at - -O 'https://example.test/downloads/sample.bin'

Dieser Befehl ist nur sinnvoll, wenn Server und Validatoren den Vertrag der Repräsentation bewahren. Ein Werkzeug kann berechnen, wo lokale Bytes enden. Es kann nicht bewirken, dass nicht passende entfernte Bytes zur selben Version gehören.

Partial Content sollte gezielt unterstützt werden

Range-Unterstützung ist wertvoll für große unveränderliche Downloads, das Springen in Medien und Clients, die tatsächlich von partiellem Abruf profitieren. Sie kann für kleine dynamische Responses, generierte Repräsentationen mit schwer stabil zu haltender Identität oder Endpunkte, bei denen eine vollständige Response einfach und günstig genug ist, unnötig sein.

Das verlässliche mentale Modell bleibt bescheiden: Ein Client fragt nach einem Teil einer ausgewählten Repräsentation; der Server darf die Anfrage erfüllen oder ignorieren; Status und Metadaten der Response teilen dem Client das Ergebnis mit; und ein starker Validator verhindert, dass ein fortgesetzter Download unbemerkt zwischen Repräsentationsversionen wechselt. Werden diese Teile als ein Vertrag behandelt, wirkt 206 Partial Content nicht länger wie ein Optimierungstrick, sondern wie das, was es ist: eine präzise Aussage darüber, welche Bytes gesendet werden.

References