param: :slug 라우트에서 손으로 조립한 URL 점검하기

블로그에 글을 올리는 스크립트에 HTTP API 경로를 추가했다. 기존 SSH 경로는 그대로 두고 둘 다 같은 JSON을 내도록 맞췄다. 출력 형식을 통일한 김에 두 경로의 결과를 나란히 찍어봤는데, 한 필드만 달랐다.

# api 경로
"admin_url": "https://blog.devnsb.com/admin/posts/thinkingcap-qwen3-6-27b/edit"

# ssh 경로
"admin_url": "https://blog.devnsb.com/admin/posts/25/edit"

id로 만든 URL은 라우트를 따라가지 못한다

이 앱의 관리자 라우트는 slug를 파라미터로 쓴다.

namespace :admin do
  resources :posts, except: :show, param: :slug
end

param: :slug를 주면 경로 세그먼트가 :id가 아니라 :slug가 되고, 컨트롤러도 그에 맞춰 조회한다.

def set_post
  @post = Post.find_by!(slug: params[:slug])
end

그런데 SSH 경로의 스크립트는 URL을 문자열로 직접 조립하고 있었다.

admin_url: "https://blog.devnsb.com/admin/posts/#{post.id}/edit"

/admin/posts/25/edit로 요청하면 params[:slug]"25"가 담긴다. 컨트롤러는 slug가 "25"인 글을 찾고, 그런 글은 없으니 RecordNotFound가 난다. 확인해 보니 숫자로만 이루어진 slug를 가진 글은 하나도 없었다. 이 스크립트가 그동안 보고한 관리자 링크는 전부 404였다.

인증 리다이렉트가 404를 가렸다

왜 여태 몰랐는지가 더 중요하다. 링크를 눌러 확인해 볼 수 있었을 텐데 증상이 드러나지 않았다.

관리자 경로는 인증을 요구한다. require_authenticationbefore_action으로 걸려 있어서 컨트롤러 액션보다 먼저 돈다. 즉 로그인하지 않은 요청은 글을 조회하기도 전에 로그인 화면으로 리다이렉트된다.

$ curl -o /dev/null -w '%{http_code}' https://blog.devnsb.com/admin/posts/25/edit
302

깨진 URL과 멀쩡한 URL이 똑같이 302를 준다. 로그인한 뒤에야 404가 나오는데, 초안을 검토하려고 링크를 누르면 로그인 화면을 거쳐 오다 보니 "세션이 풀렸나 보다" 정도로 넘어가기 쉽다. 인증이 앞단에 있는 경로는 익명 요청의 상태 코드로 정상 여부를 판단할 수 없다.

라우트 헬퍼를 쓰면 생기지 않는다

API 경로는 같은 실수를 하지 않았다. URL을 조립하지 않고 헬퍼를 썼기 때문이다.

admin_url: edit_admin_post_url(post)

헬퍼는 라우트 정의에서 파라미터 이름을 읽고 모델의 to_param을 호출한다. 이 앱의 Postto_param이 slug를 돌려주므로 자동으로 맞는 URL이 나온다. 나중에 파라미터를 다시 바꿔도 헬퍼를 쓴 자리는 따라오지만, 문자열로 박아둔 자리는 그대로 남는다.

라우트 헬퍼를 쓸 수 없는 환경이라면 최소한 무엇을 넣어야 하는지 라우트 정의에서 확인해야 한다. bin/rails routes의 경로 패턴에 :slug가 찍혀 있으면 id를 넣을 자리가 아니다.

같은 계약의 두 번째 구현이 검사기 역할을 한다

이 버그는 코드를 읽어서 찾은 게 아니다. 같은 출력을 내야 하는 구현이 두 개가 되면서, 둘을 나란히 놓자 차이가 저절로 보였다.

기존 구현 하나만 있을 때는 그 출력이 기준선이라 틀렸는지 알 방법이 없다. 두 번째 구현이 생기면 서로가 서로의 대조군이 된다. 새 경로를 추가할 때 출력 형식을 통일해 두면 마이그레이션이 쉬워질 뿐 아니라, 기존 경로에 남아 있던 오류가 드러나는 부수 효과가 따라온다.

값이 갈렸을 때 새로 만든 쪽을 의심하기 쉽지만, 이번에는 오래된 쪽이 틀렸다. 어느 쪽이 맞는지는 두 구현을 비교해서 정할 게 아니라 라우트 정의 같은 원본 정의에 물어봐야 한다.

댓글

아직 댓글이 없습니다.

댓글 남기기