배송 조회 연동에서 간헐적으로 거부가 왔다. 본문에는 이렇게만 있었다.
{"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 를 켜면 보낸 헤더와 받은 헤더가 다 나온다. 문서에 없는 헤더가 두 개 더 있었는데 한 번 보면 나온다.
정리
- 본문에 없는 정보가 헤더에 있을 수 있다
CURLOPT_HEADER를 안 켜면 상태 코드도 못 본다429는 한도를 넘겼다는 뜻이다- 헤더 필드 이름은 대소문자 구분이 없으니
strtolower로 찾는다 Retry-After가 오면 그 값을 지킨다- 성공 응답의 남은 수를 기록하면 거부 전에 대응한다
X-Rate-*는 규격이 아니라 관례라 연동마다 다르다- 우리 계산과 상대 계산이 다르면 상대가 정본이다. 실패도 한도를 먹는다
- 요청 식별자를 로그에 남기면 문의가 쉬워진다
- 오류일 때 형식이 다를 수 있으니 파싱 전에
Content-Type을 확인한다 - 처음 붙일 때
CURLOPT_VERBOSE로 전체를 한 번 찍어 본다