/quick으로 버그 수정
CQ의 quick 워크플로우를 사용하여 버그를 진단하고 수정하는 단계별 예제입니다.
시나리오
팀에서 JWT 토큰이 만료되었을 때 인증 미들웨어가 401 대신 HTTP 500을 반환한다고 보고했습니다. 오류가 로그에 나타납니다:
ERROR: token parse failed: token is expired by 2h30m0s
panic: runtime error: index out of range [0] with length 0해야 할 것:
- 문제 재현
- panic 수정 및 적절한 401 반환
- 유효성 검사를 통과하여 제출
1단계: 빠른 태스크 시작
/quick "fix: 만료된 JWT에서 auth 미들웨어 panic — 401을 반환해야 함"출력:
Task created: T-042
Title: fix: 만료된 JWT에서 auth 미들웨어 panic — 401을 반환해야 함
Scope: auto-detected (go)
Status: in_progress태스크가 즉시 클레임됩니다. /claim을 별도로 실행할 필요가 없습니다.
2단계: 버그 재현
프로덕션 코드를 건드리기 전에 panic을 유발하는 테스트를 작성합니다:
go
// internal/middleware/auth_test.go
func TestAuthMiddleware_ExpiredToken_Returns401(t *testing.T) {
// 이미 만료된 토큰 생성
token := generateTestToken(t, time.Now().Add(-3*time.Hour))
req := httptest.NewRequest("GET", "/api/data", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
handler := AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rec.Code)
}
}실행:
bash
cd c4-core && go test ./internal/middleware/... -run TestAuthMiddleware_ExpiredToken--- FAIL: TestAuthMiddleware_ExpiredToken_Returns401 (0.00s)
panic: runtime error: index out of range [0] with length 0 [recovered]
goroutine 7 [running]:
...
FAIL github.com/pilab/cq/internal/middleware [build failed]버그 확인.
3단계: 근본 원인 찾기
internal/middleware/auth.go를 열어 토큰이 파싱되는 부분을 확인합니다:
go
// 수정 전 — 버그 있는 코드
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.Header.Get("Authorization"), " ")
tokenStr := parts[1] // 헤더가 없거나 잘못된 형식이면 panic
claims, err := parseJWT(tokenStr)
if err != nil {
// 버그: err가 무시되고 실행이 계속됨
}
userID := claims.UserID // claims가 nil이면 panic
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}두 가지 버그:
parts의 범위 검사 없음 —Authorization헤더가 없거나 공백이 없으면 panicerr != nil블록에서 return하지 않음 — nil 포인터의claims.UserID로 실행 계속
4단계: 코드 수정
go
// 수정 후 — 고친 코드
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
parts := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
claims, err := parseJWT(parts[1])
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userIDKey, claims.UserID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}주요 변경사항:
SplitN(..., 2): 분할을 제한하고 공백이 포함된 토큰도 온전히 유지- 각 오류 경로 후 명시적
return - nil 포인터 역참조 없음
5단계: 테스트 실행
bash
cd c4-core && go test ./internal/middleware/... -run TestAuthMiddleware_ExpiredToken -v=== RUN TestAuthMiddleware_ExpiredToken_Returns401
--- PASS: TestAuthMiddleware_ExpiredToken_Returns401 (0.00s)
PASS
ok github.com/pilab/cq/internal/middleware 0.012s헤더 누락 케이스에 대한 테스트도 추가합니다:
go
func TestAuthMiddleware_MissingHeader_Returns401(t *testing.T) {
req := httptest.NewRequest("GET", "/api/data", nil)
// Authorization 헤더 없음
rec := httptest.NewRecorder()
AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})).ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rec.Code)
}
}bash
go test ./internal/middleware/... -v
# 모든 테스트 통과6단계: 유효성 검사
/validateRunning validations...
go-build: PASS
go-vet: PASS
go-test: PASS (42 tests, 0.8s)
All validations passed.7단계: 제출
/submitSubmitting T-042...
Commit: f3a9b21 "fix(auth): return 401 on expired/missing JWT, prevent panic"
Validation: all passed
Status: done
Task T-042 completed.8단계: 확인
/statusDone:
T-042 fix: 만료된 JWT에서 auth 미들웨어 panic f3a9b21핵심 패턴
수정 전에 항상 재현. 실패하는 테스트를 먼저 작성하면 버그가 존재함을 증명하고 나중의 회귀를 방지합니다.
한 번에 하나씩 버그 수정. 두 가지 수정(범위 검사 + 오류 return)이 같은 함수를 수정하고 같은 근본 원인을 해결하는 경우 하나의 커밋에 담아도 됩니다.
모든 테스트 경로 유효성 검사. CQ의 /validate가 전체 테스트 스위트를 실행합니다. 수정된 케이스의 테스트 통과만으로는 충분하지 않습니다 — 전체 스위트가 초록색을 유지해야 합니다.