페이지는 https://notes.example 이고, 저장 주소는 https://api.example/notes 입니다. 제목을 저장하면 브라우저는 POST로 JSON을 보냅니다. 콘솔에는 CORS 오류가 있는데 API 로그에는 POST가 없을 때가 있습니다. 반대로 로그에는 저장이 남았는데 스크립트는 응답 본문을 읽지 못합니다. 두 결과는 서버가 같은 이유로 거절해서 생기지 않습니다.
주소, 토큰 문자열 example-token, 제목 “우유”는 가상 예입니다. 영어 그림의 milk는 같은 예의 번역입니다. 이 글을 쓰면서 그 주소에 요청을 보내거나 서버 로그를 모으지는 않았습니다.
출처가 다르면 브라우저가 응답을 바로 넘기지 않습니다
출처는 스키마, 호스트, 포트를 합친 값입니다. https://notes.example 과 https://api.example 은 호스트가 달라 다른 출처입니다. http와 https, localhost의 3000번과 3001번도 다릅니다. 경로는 출처가 아닙니다.
같은 출처의 fetch는 이 검사를 거치지 않습니다. 주소 문자열로 호출하는 fetch는 다른 출처에서 기본적으로 CORS 모드입니다. 응답이 이 페이지의 출처를 허용하기 전에는 스크립트가 본문을 읽지 못합니다. 검사는 브라우저가 합니다. curl이나 서버 사이의 호출은 이 검사를 하지 않습니다. curl이 200을 보여도 브라우저 안의 스크립트가 그 본문을 읽을 수 있다는 뜻은 아닙니다.
JSON과 Authorization이 OPTIONS를 먼저 만듭니다
예전 안내가 simple request라고 부른 조건은 남아 있습니다. Fetch 표준은 그 이름을 쓰지 않고, 메서드와 헤더로 사전 요청이 필요한지 계산합니다. 메서드가 GET, HEAD, POST이고 헤더가 안전 목록에 있으면 사전 요청 없이 본 요청을 보냅니다. 그 응답을 스크립트에 넘길지는 따로 검사합니다.
안전 목록의 이름은 Accept, Accept-Language, Content-Language, Content-Type, Range입니다. Content-Type은 해석된 타입이 application/x-www-form-urlencoded, multipart/form-data, text/plain일 때만 안전합니다. application/json은 여기에 없고, 보고서용 예외에도 없습니다. Range는 bytes=256-처럼 시작이 있는 단일 범위만 안전합니다.
가상 요청은 POST /notes, Content-Type은 application/json, Authorization은 Bearer example-token, 본문은 {"title":"우유"}입니다. POST는 안전 메서드라 OPTIONS를 만드는 것은 헤더입니다. 값 하나가 128바이트를 넘거나, 안전 목록을 통과한 값의 합이 1024바이트를 넘으면 그 이름도 사전 요청 대상이 됩니다. 이 예시의 값은 그보다 짧다고 가정합니다.
OPTIONS가 통과하기 전에는 POST를 보내지 않습니다
조건에 걸리면 같은 URL로 OPTIONS가 먼저 갑니다. Access-Control-Request-Method는 POST이고, Access-Control-Request-Headers는 안전하지 않은 이름을 소문자로 정렬해 쉼표만으로 잇습니다. 이 예는 authorization,content-type 이며, 이 헤더는 뒤의 POST에 다시 붙지 않습니다.
사전 요청의 자격 증명 모드는 항상 same-origin입니다. 대상이 다른 출처이므로 쿠키나 HTTP 인증 정보를 실지 않습니다. 본 요청이 나중에 쿠키를 포함할 수 있어도 OPTIONS가 그 쿠키를 미리 보내지는 않습니다.
사전 응답은 상태가 200번대이고 CORS 검사가 통과해야 다음으로 갑니다. 401이나 404는 Access-Control-Allow-Origin이 있어도 실패하고, 그러면 본 POST는 나가지 않습니다. 로그에 POST가 없는 이유 중 하나입니다. OPTIONS가 앱 앞에서 빠지면 로그가 비어 있을 수 있으니, 빈 로그만으로 브라우저가 아무것도 보내지 않았다고 보지 않습니다.
Allow-Headers에는 authorization이라는 이름이 있어야 합니다. 이 헤더는 *로 대신되지 않습니다. content-type은 자격 증명을 포함하지 않으면 *로 허용될 수 있습니다. PUT, DELETE, PATCH는 안전 메서드가 아니므로 Access-Control-Allow-Methods가 그 메서드를 허용해야 합니다. 자격 증명을 포함하지 않으면 그 목록의 *도 메서드를 대신할 수 있습니다.
POST가 도착해도 스크립트는 본문을 못 읽을 수 있습니다
사전 요청이 통과하면 POST가 나갑니다. 본문과 Authorization은 이 요청에 있습니다. fetch의 기본 자격 증명 모드는 same-origin이라 다른 출처로는 쿠키를 보내지 않습니다. 쿠키가 필요하면 credentials를 include로 바꿉니다.
POST 응답도 다시 검사합니다. Allow-Origin이 없거나 출처와 다르면 실패하고, include가 아닌 요청에서 값이 *이면 통과합니다. include이면 값은 요청 출처와 같아야 하고 Allow-Credentials는 대소문자가 맞는 true여야 하며, Allow-Origin과 Allow-Headers와 Allow-Methods의 *는 이름을 대신하지 못합니다. 실패하면 스크립트는 본문을 읽지 못하고, 서버가 이미 저장했는지는 알 수 없습니다. 그 실패가 저장을 취소하지는 않습니다.
기본적으로 읽을 수 있는 응답 헤더는 Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma입니다. 다른 이름은 Access-Control-Expose-Headers에 적습니다. 자격 증명을 포함하지 않으면 *가 그 목록을 대신할 수 있지만, Set-Cookie와 Set-Cookie2는 스크립트에 열리지 않습니다.
Access-Control-Max-Age가 없으면 허용한 메서드와 헤더 정보는 5초 동안 캐시됩니다. 더 긴 초를 적어도 브라우저가 자체 상한으로 자를 수 있고, 그 전에 지울 수도 있습니다. 86400이 다음 날까지 사전 요청을 없애 주지는 않습니다. 캐시는 출처, URL, 자격 증명 포함 여부를 나눠 기억합니다.
콘솔의 한 줄로 서버 로그를 대체하지 않습니다
스크립트는 CORS 실패의 속사정을 읽지 못합니다. 어떤 헤더가 빠졌는지는 콘솔에서 보고, 요청이 어디까지 갔는지는 서버가 받은 메서드로 봅니다.
같이 적어 둘 값은 두 출처, POST와 application/json과 Authorization, credentials가 include인지, OPTIONS의 상태, POST의 도착, 응답의 Allow-Origin입니다. include이면 Allow-Origin의 *와 Allow-Credentials 누락을 먼저 봅니다.
다른 출처로 리다이렉트되면 Authorization은 빠집니다. 사전 요청 뒤의 따라가기는 브라우저마다 다를 수 있고, 이 글은 그 차이를 다시 실행하지 않았습니다.
콘솔만 실패했다면 서버가 POST를 거절했는지부터 묻지 않습니다. OPTIONS가 막혀 POST가 나가지 않았는지, POST는 처리됐는데 응답을 스크립트에 넘기는 헤더가 없는지를 먼저 나눕니다.
이 기록에 대화를 더해 주세요.
궁금한 점, 다른 접근, 직접 해 본 결과를 나눠 주세요.
로그인 상태 확인 중…
댓글을 불러오는 중…