Wyślij jedną poprawną odpowiedź, stopniowo zapisuj jej treść i normalnie zakończ wiadomość. Ten poradnik pokazuje, jak w Terminalu na Macu przesłać kompletny, przykładowy dokument bez traktowania każdej części jako osobnej odpowiedzi i bez używania kodu 206 Partial Content.
Zadanie polega na udostępnieniu jednej strony HTML przeznaczonej do publicznego hostingu, a następnie sprawdzeniu, czy klient otrzymuje całą jej zawartość. Demonstracja działa lokalnie i niczego nie publikuje. Potrzebujesz Terminala, curl oraz obsługiwanej wersji LTS Node.js pobranej z oficjalnej strony pobierania Node.js.
Dlaczego wysyłanie w częściach nie oznacza odpowiedzi częściowej
Znaczenie odpowiedzi i sposób ramkowania jej przesyłania odpowiadają na różne pytania. W przypadku zwykłego, poprawnego GET kod 200 OK oznacza pomyślne pobranie. 206 Partial Content oznacza, że serwer realizuje żądanie zakresowe, przesyłając wybrane części reprezentacji. Podzielenie przesyłania na kilka zapisów nie tworzy żądania zakresowego. RFC 9110: Semantyka HTTP, 206 Partial Content.
Na przykład przykładowy poradnik zawierający wstęp, opis i końcowy akapit może być dostarczany stopniowo, a mimo to pozostać jednym dokumentem. Wyślij nagłówki odpowiedzi raz, a następnie po kolei sekcje treści. Nie dodawaj nowej linii statusu HTTP przed każdą sekcją.
Kodowanie transferu chunked w HTTP/1.1 opakowuje bajty treści w części o długościach zapisanych szesnastkowo i kończy się częścią o zerowej długości oraz wymaganym pustym wierszem. Te bajty ramkowania wyznaczają granice przesyłania; nie są treścią artykułu. Nadawca nie może łączyć Content-Length z Transfer-Encoding. RFC 9112: Kodowanie transferu chunked, Content-Length.
W praktyce najlepiej pozwolić bibliotece HTTP obsłużyć ramkowanie wiadomości. Aplikacja dostarcza treść dokumentu i sygnalizuje, kiedy jest ona gotowa.
Którego mechanizmu strumieniowania użyć?
Obsługiwany mechanizm zależy od wersji HTTP. Przesyłanie kompletnej treści strumieniowo jest możliwe we wszystkich trzech poniższych wersjach, ale każda z nich stosuje inne ramkowanie.
| Wersja HTTP | Sposób przesyłania treści | Mechanizm zakończenia |
|---|---|---|
| HTTP/1.1 z kodowaniem chunked | Części poprzedzone długością | Końcowa część o zerowej długości i zamknięta sekcja końcowych nagłówków |
| HTTP/2 | Ramki DATA |
END_STREAM w końcowej ramce, wraz z zakończeniem bloku nagłówków, jeśli występuje |
| HTTP/3 | Ramki DATA |
Zakończenie odpowiedzi i zamknięcie kierunku wysyłania strumienia |
HTTP/2 wyraźnie zabrania kodowania transferu chunked z HTTP/1.1. HTTP/3 nie definiuje kodowań transferu i zabrania nagłówka Transfer-Encoding. Pozwól serwerowi obsłużyć ramkowanie właściwe dla danego protokołu, zamiast wymuszać Transfer-Encoding: chunked dla każdego połączenia. RFC 9113: Wymiana wiadomości HTTP, RFC 9114: Ramkowanie wiadomości HTTP.
Jeśli niewielki dokument jest już gotowy, zwykle najłatwiej wysłać go normalnie. Zapisywanie kolejnych części przydaje się, gdy treść powstaje z czasem. Poniższy przykład celowo rozdziela trzy zapisy w czasie, aby można było sprawdzić działanie; opóźnienia mają charakter poglądowy i nie są zaleceniem dotyczącym publikacji.
Uruchom kompletny dokument strumieniowany na Macu
Otwórz Terminal i sprawdź wymagania wstępne:
node --version
curl --version
Jeśli node jest niedostępny, zainstaluj obsługiwaną wersję LTS ze strony pobierania Node.js, a następnie otwórz nowe okno Terminala. Program korzysta wyłącznie z wbudowanych modułów i nie wymaga pakietów npm.
Wklej cały poniższy blok do Terminala. Uruchomi on serwer HTTP/1.1 pod adresem demonstracyjnym http://127.0.0.1:8080/document. Te trzy ciągi znaków razem tworzą kompletną przykładową stronę.
node <<'JS'
const http = require('node:http');
const parts = [
'<!doctype html>\n<html lang="en"><head><meta charset="utf-8">' +
'<title>Paper Shapes</title></head><body><main>\n' +
'<h1>Paper Shapes</h1>\n',
'<p>A circle, a square, and a triangle make this sample collection.</p>\n',
'<p>End of the paper shapes guide.</p>\n</main></body></html>\n'
];
const server = http.createServer((req, res) => {
res.on('error', error => console.error(error.message));
if (req.method !== 'GET' || req.url !== '/document') {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('No sample document at this path.\n');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
let index = 0;
const timer = setInterval(() => {
if (res.destroyed) {
clearInterval(timer);
return;
}
res.write(parts[index++]);
if (index === parts.length) {
clearInterval(timer);
res.end();
}
}, 300);
res.on('close', () => clearInterval(timer));
});
server.on('error', error => {
console.error(error.message);
process.exitCode = 1;
});
server.listen(8080, '127.0.0.1', () => {
console.log('Sample: http://127.0.0.1:8080/document');
});
JS
Aplikacja wywołuje writeHead() raz, write() trzy razy i end() raz. Dokumentacja Node opisuje kolejne zapisy jako części tej samej treści odpowiedzi i wymaga użycia end(), aby zakończyć każdą odpowiedź. Node obsługuje ramkowanie HTTP; nie dodawaj długości części ani końcowego zera do ciągów znaków. Dokumentacja HTTP Node.js.
To ograniczona demonstracja składająca się z trzech krótkich ciągów. W przypadku dużego lub ciągłego źródła danych uwzględnij mechanizm kontroli przepływu: jeśli write() zwraca false, wstrzymaj generowanie do chwili drain, obsługując również zamknięcie i błędy. Dokumentacja zapisywania odpowiedzi w Node.js.
Pozostaw to okno Terminala otwarte. Jeśli pojawi się komunikat EADDRINUSE, oznacza to, że port 8080 jest zajęty; wybierz inny dostępny port na serwerze i zmień go we wszystkich poniższych poleceniach klienta. Po zakończeniu zatrzymaj demonstrację, naciskając Control-C.
Sprawdź zarówno zdekodowaną treść, jak i ramkowanie HTTP chunked
W drugim oknie Terminala zażądaj dokumentu:
curl --http1.1 --include --no-buffer --max-time 5 \
http://127.0.0.1:8080/document
Oczekuj jednej odpowiedzi 200 OK, nagłówka Content-Type o wartości text/html; charset=utf-8 oraz złożonej treści strony. W tym przykładzie HTTP/1.1 odpowiedź powinna używać Transfer-Encoding: chunked. Curl domyślnie dekoduje ramkowanie transferu, więc zwykły wynik wyświetla dokument zamiast szesnastkowych rozmiarów części.
Aby sprawdzić ramkowanie, ponów żądanie z opcją --raw:
curl --http1.1 --include --no-buffer --raw --max-time 5 \
http://127.0.0.1:8080/document
Dokumentacja curl opisuje --raw jako opcję wyłączającą wewnętrzne dekodowanie treści i kodowania transferu. --no-buffer wyłącza buforowanie wyjścia przez curl, a --max-time 5 ogranicza czas działania. Dzięki tym opcjom jest to krótkie polecenie diagnostyczne, które nie czeka bez końca. Podręcznik curl.
W surowym wyniku linie z rozmiarami otaczają treść, po czym następuje końcowa część o zerowej długości. Bez tego zakończenia treść HTTP/1.1 kodowana jako chunked jest niekompletna. Przy ramkowaniu o stałej długości odebranie mniejszej liczby bajtów niż zadeklarowana również oznacza niekompletność. RFC 9112: Obsługa niekompletnych wiadomości.
Surowego wyniku używaj tylko do sprawdzania warstwy transportowej. Nie kopiuj linii z rozmiarami do pliku strony ani nie traktuj ich jako sekcji dokumentu.
Odbierz cały dokument przed jego przetwarzaniem
Jeśli kolejny krok wymaga kompletnej strony, odczytaj całą treść przed analizą, zapisaniem lub zaakceptowaniem jej w inny sposób. Standard WHATWG definiuje treść odpowiedzi jako strumień; metoda text() w całości go odczytuje i zwraca ciąg znaków, a w razie błędu odczytu odrzuca wynik. Samo odebranie nagłówków odpowiedzi nie oznacza, że treść została odebrana w całości. WHATWG Fetch Standard: Mixin Body, Przetwarzanie odpowiedzi.
Gdy serwer nadal działa, wklej cały poniższy kod klienta do drugiego okna Terminala:
node --input-type=module <<'JS'
try {
const response = await fetch('http://127.0.0.1:8080/document', {
signal: AbortSignal.timeout(5000)
});
if (response.status !== 200) {
throw new Error(`Expected 200; received ${response.status}`);
}
const html = await response.text();
const marker = 'End of the paper shapes guide.';
if (!html.includes(marker) || !html.endsWith('</html>\n')) {
throw new Error('Expected sample ending is missing');
}
console.log(html);
console.log('Complete sample document received.');
} catch (error) {
console.error('Document not accepted:', error.message);
process.exitCode = 1;
}
JS
Node udostępnia zgodną z przeglądarkami implementację fetch(). Jej AbortSignal.timeout() tworzy sygnał, który przerywa działanie po upływie określonej liczby milisekund; tutaj pięć sekund to przykładowy limit dla tego małego lokalnego przykładu. Dokumentacja obiektów globalnych Node.js, Dokumentacja limitu czasu AbortSignal.
Klient sprawdza dwie różne rzeczy: pomyślny odczyt całej treści oraz oczekiwane zakończenie przykładowego tekstu. Ten drugi test jest diagnostyką specyficzną dla aplikacji. HTTP może poprawnie przesłać dokument, który aplikacja przypadkowo skróciła przed wywołaniem end(). Znacznik zakończenia pomaga wykryć ten konkretny błąd, ale nie sprawdza każdego akapitu i nie jest ogólną walidacją HTML.
Ten klient przechowuje całą stronę w pamięci. W przypadku bardzo dużych dokumentów wybierz przetwarzanie przyrostowe z jasno określonym formatem dokumentu i regułami zakończenia. Nie zakładaj, że każdy odczyt dostarcza kompletny akapit lub samodzielnie znaczącą odpowiedź.
Zdiagnozuj zakończenie, zanim zmienisz kod statusu
Użyj tej sekwencji, aby znaleźć problem. Pozwala ona oddzielić zakończenie przesyłania od treści oczekiwanej przez aplikację, dzięki czemu pozornie pomyślne pobranie nie ukryje brakującego zakończenia.
| Obserwacja | Następne działanie |
|---|---|
| Polecenie lokalne nie może nawiązać połączenia | Sprawdź, czy serwer działa i czy port jest zgodny. |
| Curl przekracza limit czasu | Sprawdź, czy źródło danych kończy działanie i wywołuje end(); zachowaj komunikat o błędzie. |
| Transfer się kończy, ale test zakończenia nie przechodzi | Sprawdź wygenerowaną treść i miejsce, w którym aplikacja kończy odpowiedź. |
| Cały dokument dociera jednocześnie | Porównaj surowy i zdekodowany wynik; nie wyciągaj wniosków o wielu odpowiedziach na podstawie czasu wyświetlania. |
Zwykłe żądanie całej strony otrzymuje 206 |
Sprawdź, czy przypadkowo nie zastosowano obsługi zakresów; samo przesyłanie strumieniowe nie uzasadnia tego kodu. |
Testy zakończenia to zalecenia wynikające z protokołu i działania Fetch, a nie osobna funkcja HTTP. Jeśli operacja odczytu treści się nie powiedzie, klient powyżej zgłosi błąd i zakończy działanie niepowodzeniem, zamiast wyświetlać komunikat o powodzeniu dla częściowo odebranej treści.
Co to oznacza dla strony publicznej i Google Search?
Umieść przydatną treść dokumentu bezpośrednio w odpowiedzi HTML. Google wyjaśnia, że indeksowanie i analizowanie przebiega sprawnie w przypadku stron, których HTML odpowiedzi zawiera całą treść, oraz zaleca renderowanie po stronie serwera lub prerenderowanie, ponieważ nie każdy bot wykonuje JavaScript. Przykład stosuje to podejście: tekst dociera bezpośrednio w treści dokumentu. Google Search Central: Podstawy SEO w JavaScript.
Dokumentacja robotów Google mówi, że odpowiedź 200 przekazuje odebraną treść do kolejnego etapu przetwarzania; nie gwarantuje to indeksowania. Przywołana dokumentacja nie określa specjalnego rozmiaru części ani opóźnienia, które zapewniałyby indeksowanie strony. Traktuj strumieniowanie jako sposób dostarczania treści, a jej kompletność sprawdzaj osobno. Google: Wpływ kodów statusu HTTP na roboty Google.
Przenosząc tę demonstrację na hosting publiczny, powtórz testy dla rzeczywistego adresu dokumentu. Potwierdź poprawny status, odczytaj całą treść i porównaj jej końcową wersję z zamierzoną stroną. Kontrolę poprawności oprzyj na zawartości dokumentu, niezależnie od liczby zapisów, ramek czy widocznych aktualizacji, przez które został przesłany.
Źródła
- RFC 9110: Semantyka HTTP — RFC Editor / IETF.
- RFC 9112: HTTP/1.1 — RFC Editor / IETF.
- RFC 9113: HTTP/2 — RFC Editor / IETF.
- RFC 9114: HTTP/3 — RFC Editor / IETF.
- Fetch Standard — WHATWG.
- HTTP — Node.js.
- Obiekty globalne — Node.js.
- Pobierz Node.js — Node.js.
- curl — jak używać — curl.
- Podstawy SEO w JavaScript — Google Search Central.
- Wpływ kodów statusu HTTP na roboty Google — Google for Developers.