문제: 외부 API 요청이 응답 전에 실패하면, Go의
*url.Error가 쿼리 문자열에 실린 계좌번호까지 로그에 남겼습니다.
해결: 에러의 URL에서 쿼리를 통째로 잘라 같은*url.Error로 다시 만들고, 테스트에는 남아야 할 것도 적었습니다.
실제 계좌로 전환하기 전에 계좌번호가 평문으로 보이는 곳을 점검했더니, 로그 파일 한 곳에서 4,899줄이 나왔습니다. 테스트용 계좌 두 개의 번호였습니다.
Go 1.25 · http.Client Timeout 기준
계좌 조회는 GET 요청이라 계좌번호가 쿼리 문자열에 실립니다. Go의 http.Client는 응답 전에 실패하면(timeout, connection refused) *url.Error를 돌려주고, 그 URL 필드에는 주소가 쿼리까지 들어 있습니다. 표준 라이브러리가 지우는 건 주소 속 비밀번호뿐입니다. 저는 이 에러를 감싸 경고 로그로 남기고 있었습니다.
Get "https://api.example.com/v1/summary?account=<acct>": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
비밀 값을 가리는 함수는 있었지만 메신저 알림 경로에만 붙어 있었고, 가리는 대상도 봇 토큰 하나였습니다. 로그 파일로 가는 조회 실패 에러는 그 함수를 거치지 않았습니다.
가릴 파라미터 목록 대신 쿼리 문자열을 통째로 버렸습니다. 목록은 새 값이 붙을 때마다 고쳐야 하고, 한 번 잊으면 다시 샙니다. 대가로 다른 조회의 쿼리 조건도 로그에서 빠집니다.
에러는 문자열로 바꾸지 않고 같은 *url.Error로 다시 만들었습니다. Op와 Err가 그대로라 errors.As나 Timeout()으로 원인을 묻는 코드는 전과 같은 값을 받습니다(재구성한 코드).
func redactURLError(err error) error {
var ue *url.Error
if !errors.As(err, &ue) {
return err
}
u := ue.URL
if i := strings.IndexByte(u, '?'); i >= 0 {
u = u[:i]
}
return &url.Error{Op: ue.Op, URL: u, Err: ue.Err}
}
같은 메시지에는 200이 아닌 응답의 body도 통째로 실리고 있었습니다. 계좌번호가 돌아온 적은 없지만, 응답이 보낸 값을 되돌려 줄지는 제가 못 정합니다. 그래서 오류 코드 같은 진단 필드만 남기고, 형식을 모르는 body는 길이만 적습니다.
고치기 전에 실패하는 테스트 세 개를 썼습니다(connection refused, 재시도 중 timeout, 서버가 계좌번호를 되돌려 줌). 셋 다 메시지에 계좌번호·필드 이름·?가 없는지 봅니다. 그런데 없어야 할 것만 보면 전부 지우는 코드도 통과합니다. 그래서 refused는 경로와 원인이, 되돌려 주는 경우는 오류 코드가 남는지도 확인합니다.
이 수정은 새로 쌓이는 줄만 막습니다. 이미 찍힌 줄은 따로 정하기로 했고, 배포 뒤 다시 세어 늘었는지 볼 계획입니다.
외부 호출 에러를 로그에 남기기 전에, URL을 통째로 들고 있는지부터 보시길 권합니다.
같은 사례를 화면으로 따라가는 영상