RLidea
한국어
기술 기록

ads.txt 가 charset=utf8mb4 로 나갔다 — nginx 전역 설정의 함정

nginx 가 정적 파일을 text/plain; charset=utf8mb4 로 내보내고 있었다. utf8mb4 는 MySQL 인코딩 이름이지 HTTP charset 이 아니다. 전역 설정의 한 줄을 찾아 서버 블록에서 덮은 기록.

애드센스를 신청하려고 ads.txt 응답 헤더를 확인하다가 이상한 것을 봤다.

content-type: text/plain; charset=utf8mb4

utf8mb4. 어디서 많이 본 이름이다 — MySQL 설정에서. 그런데 HTTP 헤더에서 볼 이름은 아니다.

답부터 적는다. nginx 전역 설정에 박혀 있던 charset utf8mb4; 한 줄이 원인이었고, 사이트의 서버 블록에서 charset utf-8; 로 덮어 해결했다. 전역을 고치지 않은 이유까지 포함해 과정을 남긴다.

증상

이 서버(Oracle Cloud ARM 1코어)는 원래 centell.me 라는 NestJS 앱이 살던 집이고, rlidea.app 은 정적 사이트로 나중에 입주했다. 배포하고 헤더를 점검하는데 nginx 가 직접 서빙하는 정적 파일마다 charset=utf8mb4 가 붙어 나왔다.

보통 파일이면 미루고 넘어갔을지도 모른다. 문제는 그게 ads.txt 였다는 것. 애드센스가 주기적으로 받아서 파싱하는 파일이다. 브라우저는 모르는 charset 을 만나면 대체로 너그럽게 넘어가 주지만, 자동 파서가 비표준 라벨을 어떻게 받을지는 알 수 없다. 심사를 넣기 전에 치우는 게 맞다.

사태 파악

서버에 붙어 charset 을 찾아보았다.

$ grep -n 'charset' /etc/nginx/nginx.conf
21:	charset utf8mb4; # 헤더에 charset 추가

주석까지 친절하게 달려 있는 걸 보니 과거의 내가 일부러 넣은 줄이다. centell.me 를 세팅하던 시절, 데이터베이스를 utf8mb4 로 맞추던 흐름에서 HTTP charset 까지 같은 이름으로 적은 모양이다.

원인

utf8mb4 는 MySQL 이 쓰는 인코딩 이름이다. MySQL 의 옛 utf8(별칭 utf8mb3)이 3바이트까지만 담는 반쪽짜리라서, 4바이트 전체를 담는 진짜 UTF-8 에 utf8mb4 라는 이름을 따로 붙였다. MySQL 바깥에서 utf8mb4 에 해당하는 것은 그냥 UTF-8 이다.

HTTP 의 charset 은 IANA 등록 이름을 쓰고, 거기에 utf8mb4 는 없다. 올바른 라벨은 utf-8 이다.

그리고 nginx 는 charset 지시어에 적힌 값을 검증하지 않는다. 적은 대로 헤더에 붙여 내보낼 뿐이다. ads.txt 의 text/plain 은 charset 모듈이 기본으로 손대는 타입(charset_types 기본값)에 들어 있어서, 이 전역 한 줄이 그대로 애드센스가 읽는 파일까지 닿았다.

해결

  1. 전역은 고치지 않기로 했다. nginx.conf 의 그 줄을 utf-8 로 바꾸면 끝이지만, 그 설정 위에 centell.me 가 돌고 있다. 한 줄 고치자고 멀쩡히 서비스 중인 사이트까지 검증 범위에 넣고 싶지 않았다.

  2. 대신 rlidea.app 서버 블록 안에서 덮었다. 같은 지시어를 더 안쪽 블록에 적으면 그 블록에선 안쪽 값이 이긴다.

    # 전역 nginx.conf 의 `charset utf8mb4;`(MySQL 인코딩명, HTTP 비표준)를 이 사이트에서만 덮는다.
    charset utf-8;
    charset_types text/plain text/css application/javascript application/json application/xml text/xml image/svg+xml;
  3. nginx -t 로 검사하고 reload.

  4. 확인.

    $ curl -sI https://rlidea.app/ads.txt | grep -i content-type
    content-type: text/plain; charset=utf-8

왜 여태 안 보였나

이 줄은 몇 년을 전역에 있었는데 centell.me 는 멀쩡했다. 왜인가 싶어 재 보았다.

$ curl -sI https://centell.me/robots.txt | grep -i content-type
content-type: text/plain; charset=UTF-8

centell.me 의 응답은 앱(Express)이 Content-Type 에 charset 을 직접 박아서 나온다. nginx 의 charset 모듈은 프록시한 응답에 이미 charset 이 있으면 건드리지 않는다(override_charset 기본값이 off 다). 그래서 앱이 서던 자리에서는 전역 utf8mb4 가 한 번도 겉으로 드러난 적이 없고, nginx 가 파일을 직접 내보내는 정적 사이트가 입주하자마자 나타났다.

전역 설정의 함정은 이렇다 — 잘못 적은 날 터지는 게 아니라, 조건이 맞는 세입자가 들어온 날 터진다. 그 사이의 몇 년이 「이 설정은 검증됐다」는 착각을 만든다.

참고: