Skip to content
isdnetworks
Go back

응답 헤더에 남은 횟수가 있었다

배송 조회 연동에서 간헐적으로 거부가 왔다. 본문에는 이렇게만 있었다.

{"code":"E999","msg":"처리할 수 없습니다"}

무엇 때문인지 언제 다시 해야 하는지가 없다.

Table of contents

Open Table of contents

원인 — 헤더를 안 받고 있었다

요청 코드를 봤다.

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($ch);

CURLOPT_HEADER 가 없어서 본문만 온다. 켜고 다시 봤다.

HTTP/1.1 429 Too Many Requests
X-Rate-Limit: 100
X-Rate-Remaining: 0
X-Rate-Reset: 1412838000
Retry-After: 180

한도와 남은 수와 초기화 시각과 재시도 대기 시간이 다 있었고 상태 코드도 그때 보였는데 429 Too Many Requests 였다.

본문에 없는 것이 헤더에 있었다. 상대가 준 정보를 절반만 쓰고 있었던 것이다.

헤더를 해석하게 했다

헤더와 본문을 잘라 쓰게 바꿨다.

curl_setopt($ch, CURLOPT_HEADER, true);
$res = curl_exec($ch);

$hsize  = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$rawHdr = substr($res, 0, $hsize);
$body   = substr($res, $hsize);

$headers = [];
foreach (preg_split("/\r?\n/", $rawHdr) as $line) {
    if (strpos($line, ':') === false) continue;
    list($k, $v) = explode(':', $line, 2);
    $headers[strtolower(trim($k))] = trim($v);
}

strtolower 로 키를 통일했다. HTTP 규격이 필드 이름을 대소문자 구분 없는 것으로 정하고 있어서 서버마다 표기가 다르게 온다. 리다이렉트가 있으면 헤더 블록이 여러 개 오는데 마지막 것만 쓰게 처리했다.

$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($code == 429) {
    $wait = isset($headers['retry-after']) ? (int)$headers['retry-after'] : 60;
    log_message('warning', "한도 초과. {$wait}초 대기");
    return ['retry_after' => $wait];
}

임의 간격으로 다시 부르면 또 거부되고 한도를 더 깎는다. Retry-After 가 오면 그 값을 지키게 했다.

성공 응답에도 남은 수가 오므로 그것도 기록했다.

if (isset($headers['x-rate-remaining'])) {
    $remain = (int)$headers['x-rate-remaining'];
    $this->cache->save('api_remain', $remain, 300);

    if ($remain < 20) {
        log_message('warning', "연동 잔여 {$remain}");
    }
}

거부되기 전에 알 수 있고 그때 요청 간격을 늘린다.

우리 계산과 상대 계산

우리가 센 요청 수와 상대가 준 남은 수가 안 맞았다.

우리 계산: 오늘 62건 발송 → 남은 38건
상대 헤더: 남은 12건

차이가 26건이었는데 확인해 보니 실패한 요청도 한도를 먹고 있었고 우리는 성공만 세고 있었다.

어긋나면 상대가 주는 값이 정본이므로 우리 계산을 버리고 헤더 값을 쓰게 바꿨다.

이 이름은 규격에 있는 것이 아니라 관례다. 429 를 정의한 문서도 세는 방법과 알리는 방법은 서버에 맡긴다고만 적어 뒀다. 그러니 연동마다 이름이 다를 수 있어 처음에 확인해야 한다.

처음 붙일 때 전체를 찍어 본다

한도 말고도 유용한 것이 있었다. 요청 식별자는 상대가 붙인 번호라 문의할 때 이걸 주면 상대가 자기 로그에서 찾는다.

log_message('info', "req_id={$headers['x-request-id']} code={$code}");

없으면 시각과 내용으로 설명해야 한다. 오류일 때 본문 형식이 성공일 때와 다른 경우도 있어서 파싱 전에 확인하게 했다.

if (strpos($headers['content-type'] ?? '', 'json') === false) {
    log_message('error', "예상과 다른 형식: " . substr($body, 0, 200));
    return null;
}

서버 시각도 우리 서버와 크게 다르면 서명 검증이 실패할 수 있어 함께 봤다.

연동을 새로 붙일 때 요청과 응답을 통째로 한 번 찍어 보게 했다.

curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, fopen('/tmp/curl.log', 'w'));

CURLOPT_VERBOSE 를 켜면 보낸 헤더와 받은 헤더가 다 나온다. 문서에 없는 헤더가 두 개 더 있었는데 한 번 보면 나온다.

정리


Share this post on:

Previous Post
같은 파일인데 한쪽에서만 404였다
Next Post
온 김에 더 고칠지 정했다