1. คลังความรู้
  2. HTTP와 웹
  3. HTTP 상태 코드

อัปเดต 1 เดือนที่ผ่านมา

대부분의 HTTP 오류는 당신이 무언가 잘못 말했을 때 발생합니다. 하지만 이 오류는 아무 말도 하지 않았을 때 발생합니다.

408 Request Timeout은 클라이언트가 요청을 시작했지만 제때 완료하지 못했을 때 발생합니다. 서버는 문을 열고 기다렸습니다. 당신은 말을 시작했다가 갑자기 침묵했습니다. 결국, 서버는 포기했습니다.

포기의 해부

정상적인 요청은 다음과 같이 흐릅니다:

Client: POST /api/data HTTP/1.1
Client: Content-Type: application/json
Client: Content-Length: 1024
Client: 
Client: {"name": "example", ...}
Server: HTTP/1.1 200 OK

408은 이 흐름이 멈출 때 발생합니다:

Client: POST /api/data HTTP/1.1
Client: Content-Type: application/json
Client: Content-Length: 1024
Client: 
[침묵]
[더 긴 침묵]
Server: HTTP/1.1 408 Request Timeout
Server: Connection: close

클라이언트는 1024바이트를 보내겠다고 약속했습니다. 서버는 기다렸습니다. 하지만 바이트는 끝내 오지 않았습니다.

요청이 침묵하는 이유

네트워크가 중간에 끊어졌습니다. 파일을 업로드하는 도중 WiFi가 끊기면, 절반쯤 전송된 요청이 공중에 붕 뜨게 됩니다.

클라이언트가 멈췄습니다. 브라우저 탭이 충돌하거나, 앱이 잠기거나, 스크립트가 무한 루프에 빠지면 서버는 영영 오지 않을 데이터를 기다리게 됩니다.

연결 속도가 너무 느립니다. 56kbps 연결로 100MB 파일을 업로드하면, 서버가 기다릴 수 있는 시간보다 실제로 더 오래 걸릴 수 있습니다.

클라이언트가 본문 전송을 빠뜨렸습니다. 헤더에는 Content-Length: 1024라고 되어 있지만, 코드가 실제로 그 1024바이트를 보내지 않는 경우입니다.

서버 타임아웃: 인내심 설정

서버는 얼마나 기다릴지 설정합니다:

# Nginx
client_header_timeout 60s;   # 헤더를 받기까지의 시간
client_body_timeout 60s;     # 본문을 받기까지의 시간
keepalive_timeout 75s;       # 다음 요청을 기다리는 시간

이 설정들은 임의적인 것이 아닙니다. 시작은 되었지만 끝나지 않는 연결로부터 서버를 보호하기 위한 것으로, 그런 연결이 방치되면 서버 자원을 무한정 소비하게 됩니다.

대용량 업로드의 경우에는 더 넉넉한 시간이 필요합니다:

location /api/upload {
    client_body_timeout 300s;  # 업로드를 위한 5분
}

408 vs. 504: 방향이 중요합니다

두 오류 모두 타임아웃과 관련이 있지만, 방향은 정반대입니다:

408 Request Timeout: 서버가 당신의 말이 끝나기를 기다리고 있습니다.

당신 → (침묵) → 서버 → 408

504 Gateway Timeout: 게이트웨이가 업스트림 서버의 응답을 기다리고 있습니다.

당신 → 게이트웨이 → (대기 중) → 업스트림 서버
              ↓
             504

408은 전송에 관한 것입니다. 504는 수신에 관한 것입니다.

클라이언트로서 408 처리하기

많은 4xx 오류와 달리, 408은 재시도해도 안전합니다. 문제는 요청 자체가 아니라 요청이 도달하지 않았다는 것입니다. 다시 시도해 보세요:

async function fetchWithRetry(url, options, maxRetries = 3) {
    for(let attempt = 0; attempt < maxRetries; attempt++) {
        const response = await fetch(url, options);
        
        if(response.status === 408 && attempt < maxRetries - 1) {
            await sleep(Math.pow(2, attempt) * 1000); // 지수 백오프
            continue;
        }
        
        return response;
    }
}

대용량 업로드의 경우, 버퍼링 대신 스트리밍을 사용하세요:

// 전체 파일을 메모리에 올리지 마세요
const stream = fs.createReadStream('large-file.bin');
await fetch('/api/upload', { method: 'POST', body: stream });

408을 잘못 사용하는 경우

느린 처리에 408을 사용하지 마세요. 서버가 요청을 받았지만 응답을 생성하는 데 너무 오래 걸린다면, 그것은 503 또는 504이지 408이 아닙니다.

// 잘못됨: 느린 것은 서버이지 클라이언트가 아님
HTTP/1.1 408 Request Timeout

// 올바름: 서버에 과부하가 걸린 경우
HTTP/1.1 503 Service Unavailable

잘못된 데이터에 408을 사용하지 마세요. 클라이언트가 이상한 데이터를 보냈다면, 그것은 400 Bad Request입니다. 408은 클라이언트가 충분히 보내지 않은 것이지, 보낸 내용이 잘못된 것이 아닙니다.

올바른 408 응답

HTTP/1.1 408 Request Timeout
Connection: close
Retry-After: 60
Content-Type: application/json

{
    "error": "Request Timeout",
    "message": "The server closed the connection after waiting for your request. You can retry."
}

Connection: close 헤더는 중요합니다. 타임아웃 이후에는 연결 상태가 불확실하기 때문에, 새로 시작하는 편이 좋습니다.

핵심 진실

408은 존재가 아닌 부재에 관한 유일한 HTTP 오류입니다. 다른 모든 오류는 당신이 잘못 한 무언가에 반응합니다. 이 오류만은 당신이 아무것도 하지 않은 것에 반응합니다.

서버는 기다렸습니다. 당신은 나타나지 않았습니다. 서버는 다음으로 넘어갔습니다.

408 Request Timeout에 관한 자주 묻는 질문

408 오류를 재시도할 수 있나요?

네. 잘못된 데이터(400)나 인증 누락(401)으로 인한 오류와 달리, 408은 단순히 요청이 제때 도달하지 못했다는 의미입니다. 요청 자체가 거부된 것이 아니라 완전히 수신되지 않은 것입니다. 지수 백오프를 적용하여 재시도하세요.

업로드가 계속 타임아웃되는 이유는 무엇인가요?

파일이 서버의 기본 타임아웃 시간에 비해 너무 클 가능성이 높습니다. 업로드 엔드포인트에 대해 서버의 client_body_timeout을 늘리거나, 각 청크가 빠르게 완료되도록 청크 단위로 스트리밍하여 업로드하세요.

클라이언트 타임아웃과 408의 차이점은 무엇인가요?

클라이언트 타임아웃은 당신의 코드가 응답을 기다리다 포기한 것입니다. 408은 서버가 당신의 요청을 기다리다 포기한 것입니다. 둘은 정반대입니다. 하나는 수신에 관한 것이고, 다른 하나는 전송에 관한 것입니다.

처리 시간이 너무 오래 걸리면 서버가 408을 반환해야 하나요?

아니요. 408은 구체적으로 요청 전송 시간에 관한 것입니다. 서버가 완전한 요청을 수신했지만 처리 속도가 느리다면, 503 Service Unavailable을 반환하거나 프록시가 504 Gateway Timeout을 반환하도록 하세요.

หน้านี้มีประโยชน์หรือไม่?

😔
🤨
😃