Skip to content
isdnetworks
Go back

200인데 본문이 비어 있었다

수집기가 하루치를 돌고 나면 목록이 남는데 어떤 항목만 계속 안 들어왔다. 로그를 열어 보니 오류가 하나도 없었다. 요청도 나갔고 응답도 왔고 HTTP 상태는 200 이었다.

Table of contents

Open Table of contents

성공 코드가 내용을 보장하지 않았다

응답을 파일로 떨궈서 열어 봤다.

<html><head></head><body></body></html>

이게 전부였다. 길이는 40바이트 남짓이고 상태 코드는 200 이다. 수집기 코드는 이렇게 돼 있었다.

$html = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($code != 200) {
    log_error("수집 실패: {$url}");
    return false;
}

$items = parse_items($html);   // 빈 배열

CURLINFO_HTTP_CODE 만 본다. 200 이면 통과다. 그 뒤 parse_items 는 빈 문서에서 아무것도 못 꺼내고 빈 배열을 돌려준다.

상태 코드는 연결과 응답이 이루어졌다는 뜻이지 내용이 왔다는 뜻이 아니었다. 그 빈 배열은 정말 항목이 없는 날과 구분되지 않는다.

실패가 정상으로 기록됐다

이게 제일 나빴다. 오류로 안 남으니까 아무도 모른다.

오류 로그   비어 있음
수집 로그   "0건"
재시도      안 함 (실패로 안 봤으니까)

며칠치가 그렇게 지나갔다. 사람이 목록을 보다가 이 항목이 왜 없느냐고 물어서야 드러났다.

실패가 실패로 안 보이면 재시도 장치도 안 돈다. 재시도 조건이 $code != 200 이었기 때문이다. 조용히 실패하는 것이 요란하게 실패하는 것보다 훨씬 오래 살아남는다.

원인 — 왜 빈 문서가 왔나

같은 주소를 브라우저로 열면 내용이 나오는데 수집기로 부르면 비었다. 차이를 하나씩 지워 봤다.

조건결과
헤더 없이 요청빈 본문
User-Agent 를 브라우저 것으로빈 본문
Referer 추가빈 본문
쿠키를 유지하고 두 번째 요청내용 있음

첫 요청에서 세션을 만들고 두 번째 요청부터 내용을 준다. 첫 요청은 세션만 발급하고 빈 껍데기를 돌려준다.

그쪽 입장에서는 오류가 아니다. 세션이 없는 요청에 빈 문서를 주는 것이 그쪽 동작이라 200 인 것이다. 잘못된 요청이라면 4xx 를, 본문이 없다는 뜻이면 204 를 줄 자리인데 그렇게 하지 않았다. 상대의 처리 방식은 우리가 정할 수 없으니 받는 쪽에서 판정을 세우는 수밖에 없었다.

내용으로 세운 판정 순서

고칠 지점은 요청이 아니라 판정이었다.

$html = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($code != 200) {
    return fail("상태 코드 {$code}");
}
if (strlen($html) < 500) {
    return fail("본문 길이 " . strlen($html));
}
if (strpos($html, '<div class="list') === false) {
    return fail("목록 영역 없음");
}

CURLINFO_HTTP_CODEstrlen 으로 잰 본문 길이와 <div class="list 라는 표지 셋이다. 세 번째가 핵심인데 목록이 있는 문서라면 목록을 감싸는 요소가 반드시 있고 그게 없으면 파싱 이전에 이미 실패다.

strlen 만으로는 부족했다. 오류 안내 문구가 든 문서는 길이가 충분한데 목록은 없다.

parse_items 가 이 판정을 통과한 뒤에 빈 배열을 주면 그날 항목이 정말 없는 것이고 통과 못 한 것은 문서를 못 받은 것이다. 로그에도 둘을 다르게 적었다.

[수집] 2014-03-15 0건 (정상)
[실패] 2014-03-15 본문 길이 41

이렇게 나뉘고 나서야 재시도 대상이 정해진다. 무한정 다시 부르면 상대 서버에 부담이 가니 횟수와 간격도 뒀다.

for ($i = 0; $i < 3; $i++) {
    $r = fetch($url);
    if ($r['ok']) break;
    sleep(5 * ($i + 1));
}

세 번 다 실패하면 일시적인 것이 아니다. 그때는 사람이 봐야 하니 실패 목록에 남긴다.

정리


Share this post on:

Previous Post
이 값이 어디서 오는지 몰랐다
Next Post
떠 있는 것과 응답하는 것은 다르다