코드 블록 언어 이름 하나 때문에 글 전체가 400으로 튕겼다

마크다운 11편을 노션에 올리는 스크립트를 돌렸다. 7편은 올라가고 4편이 실패했다.

실패  17mb-to-830kb: /pages → 400 validation_error: body failed validation:
body.children[11].code.language should be `"abap"`, `"abc"`, `"agda"`, ...
instead was `"astro"`.

허용 값 목록이 90개쯤 나열된 뒤 맨 끝에 실제 값이 찍힌다. 메시지가 길다. 앞부분만 읽어서는 뭐가 문제인지 안 보인다.

여기서 대개 의심하는 것

7편은 되고 4편만 안 되니 그 4편에 뭔가 있다고 생각했다. 길이를 재봤다. 블록 수를 세봤다. 상관없었다.

실패한 파일의 공통점을 찾다가 알았다. 넷 다 코드 블록에 ```js```astro 를 썼다.

실제 원인

노션 code 블록의 language 는 정해진 값만 받는다. 자유 문자열이 아니다.

목록에 javascript 는 있는데 js 는 없다. astrovuesvelte 도 없다. 마크다운에서 습관처럼 쓰는 약칭은 전부 거부된다.

그리고 한 값이 틀리면 요청 전체가 무효가 된다. 문제가 된 코드 블록만 빠지는 게 아니다. 그 글이 통째로 안 올라간다. body.children[11] 은 11번째 블록 하나를 가리키는데 그것 때문에 나머지 40개 블록도 같이 죽는다.

처방

보내기 전에 값을 걸러낸다. 흔한 별칭은 옮기고 모르는 건 안전한 기본값으로 떨어뜨린다.

const LANG_ALIAS = {
  js: "javascript", jsx: "javascript",
  ts: "typescript", tsx: "typescript",
  sh: "shell", zsh: "shell", console: "shell",
  py: "python", yml: "yaml", md: "markdown",
  jsonc: "json", text: "plain text", txt: "plain text",
  astro: "html", vue: "html", svelte: "html",
  dockerfile: "docker",
};

const LANG_OK = new Set([
  "bash", "c", "css", "diff", "docker", "go", "graphql", "html", "java",
  "javascript", "json", "markdown", "plain text", "powershell", "python",
  "ruby", "rust", "shell", "sql", "swift", "toml", "typescript", "xml", "yaml",
]);

function codeLang(raw) {
  const k = raw.toLowerCase().trim();
  if (!k) return "plain text";
  const mapped = LANG_ALIAS[k] || k;
  return LANG_OK.has(mapped) ? mapped : "plain text";
}

모르는 값을 그대로 보내지 않는 게 핵심이다. plain text 로 떨어뜨리면 하이라이팅만 없어지고 글은 올라간다. 하이라이팅 하나 때문에 글 전체를 잃는 것보다 낫다.

고치고 다시 돌렸다.

올림 4 · 건너뜀 7 · 전체 11

왜 헷갈리나

로컬에서는 아무 문제가 없다. 마크다운 렌더러는 모르는 언어 이름을 받으면 하이라이팅만 건너뛰고 오류를 안 낸다. 그래서 ```js 를 몇 년 쓰고도 모른다.

실패가 일부에 그치는 것도 함정이다. 전부 실패했다면 스크립트를 의심했을 텐데 7:4 로 갈리니 파일 쪽을 뒤지게 된다. 오류 메시지도 도움이 안 된다. 진짜 정보가 허용 값 목록에 파묻힌다. 봐야 할 건 맨 끝의 instead was 하나다.

다른 API 에서도 같다

값 화이트리스트는 흔하다. 색상이나 아이콘 이름처럼 미리 정해진 열거형, 통화 코드와 국가 코드와 로케일, 파일 형식과 MIME 타입. 다 겪어봤다.

보내는 값이 사용자 입력이나 외부 파일에서 온다면 반드시 걸러야 한다. 내가 직접 타이핑한 값만 보낼 때는 문제가 안 생긴다. 마크다운 파일 100개를 읽어 보내는 순간 통제가 사라진다.

남은 것

노션 API 는 잘못된 블록 하나를 건너뛰고 나머지를 저장하는 옵션이 없다. 전부 아니면 전무다. 대량 업로드에서는 꽤 불편하다. 트랜잭션 관점에서는 그쪽이 맞는 설계이긴 하다.

← 블로그 목록