iSpring API returns 400 — missing error details in logs
RCA: iSpring create user failed: 400 Bad Request
Overview#
What Happened#
2026-04-28 22:55~22:56 UTC (약 38초간) cupixworks-api 서비스의 ap-southeast-2 리전에서 iSpring LMS 사용자 생성 API 호출이 16회 연속 400 Bad Request로 실패했다. 영향 받은 사용자는 Unispace 조직의 2명(mike.lee@unispace.com, jason.bussas@unispace.com)이며, 동일 사용자에 대해 약 2초 간격으로 반복 호출되었다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | RestClient::BadRequest (caught as StandardError) |
| exception.message | iSpring create user failed: 400 Bad Request |
| top_frame | IspringOperation.create_user |
| env | production, ap-southeast-2 |
| deploy | 20260428T0823Z0, 20260428T0241Z0 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
Unispace (@unispace.com) |
16 | iSpring LMS 계정 생성 실패 — 2명의 사용자가 학습 플랫폼에 접근 불가 |
Timeline#
- 2026-04-28T22:55:38Z — 최초 에러 발생.
mike.lee@unispace.com및jason.bussas@unispace.com에 대한 iSpring 계정 생성 시도 시작 - 2026-04-28T22:56:16Z — 마지막 에러 발생. 38초간 총 16회 실패 (성공 0건)
- 2026-04-29 — Error Sweeper 감지 및 RCA 분석
Error Log#
iSpring create user failed: 400 Bad Request
Impact#
- Service:
cupixworks-api - 발생 횟수: 16
- 최초 발생: 2026-04-28T22:55:38.424Z
- 최근 발생: 2026-04-28T22:56:16.438Z
Root Cause Summary#
iSpring Learn API (POST https://api-learn.ispringlearn.com/user)가 요청 본문의 특정 필드 값을 거부하여 400 Bad Request를 반환했다. 가장 유력한 원인은 해당 이메일 주소로 이미 iSpring 측에 사용자가 존재하는 경우이다. 현재 코드의 중복 검사(ARG30003)는 로컬 DB(ispring_users 테이블)만 확인하고 iSpring API 측 상태는 확인하지 않기 때문에, iSpring에 이미 존재하지만 로컬 DB에는 기록이 없는 사용자에 대해 생성 요청이 반복 시도된다. 또한 IspringOperation.create_user의 에러 핸들링이 StandardError로 포괄적으로 catch하면서 iSpring API 응답 본문을 로깅하지 않아, 정확한 거부 사유를 파악할 수 없다.
Technical Analysis#
Code Path#
- Entry point:
Api::V1::UsersController#create_ispring_account—app/controllers/api/v1/users_controller.rb:96 - Repository:
UserRepository#create_ispring_account—app/repositories/user_repository.rb:271 - Service:
Cupix::IspringService.create_user_account—app/services/cupix/ispring_service.rb:3 - Operation (HTTP call):
IspringOperation.create_user—app/operations/ispring_operation.rb:2 - Failure point:
app/operations/ispring_operation.rb:36—rescue StandardError => e
1. 로컬 DB 중복 검사 (서비스 레이어)
existing_user = User.joins(:ispring_user).where(email: user.email).first
raise Cupix::Errors::Parameter.new(code: 'ARG30003', reason: 'User already has iSpring account', message: "User with email '#{user.email}' already has an iSpring account") if existing_user.present?
이 검사는 로컬 ispring_users 테이블에만 의존한다. iSpring API 측에 이미 사용자가 존재하지만 로컬 DB에 레코드가 없는 경우(예: 직접 iSpring 콘솔에서 생성, 이전 생성 시 DB 저장 실패, 다른 환경에서 생성)에는 이 검사를 통과하여 API 호출로 진행된다.
2. iSpring API 호출 및 에러 핸들링
begin
url = 'https://api-learn.ispringlearn.com/user'
response = RestClient.post(url, request_body.to_json, headers)
rescue StandardError => e
Cupix::Logger.error("iSpring create user failed: #{e.message}", class: self.name, function: __method__)
raise Cupix::Errors::BadGateway.new(
code: 'BG10001',
reason: "iSpring create user failed: #{e.message}",
message: e.message
)
end
핵심 문제: RestClient::BadRequest (HTTP 400)를 StandardError로 포괄 catch하면서 e.response.body를 로깅하지 않는다. iSpring API가 반환하는 상세 에러 메시지(중복 이메일, 유효하지 않은 필드 등)가 모두 유실된다.
3. 같은 파일의 update_user_role 메서드와 비교
rescue RestClient::Exception => e
error_body = e.response&.body || ''
Cupix::Logger.error("iSpring update user role failed: #{e.message}", class: self.name, function: __method__, response: error_body, request_body: request_body)
update_user_role과 create_department 메서드는 RestClient::Exception을 먼저 catch하여 응답 본문(e.response.body)과 요청 본문(request_body)을 모두 로깅한다. create_user만 이 패턴을 따르지 않는 불일치가 존재한다.
4. 요청 본문 구조
request_body = {
departmentId: department_id,
password: temporary_password,
fields: {
login: user.email,
email: user.email,
first_name: user.firstname,
last_name: user.lastname
},
sendLoginEmail: true
}.merge(
user.has_admin_permission?(user.team) ? {
role: 'department_administrator',
manageableDepartmentIds: [department_id]
} : {}
)
login과 email 모두 user.email을 사용한다. iSpring API에서 login 또는 email이 이미 등록된 경우 400 Bad Request를 반환할 수 있다.
Log Evidence#
Datadog 검색 쿼리:
service:cupixworks-api status:error @environment:production "iSpring create user failed"
에러 로그 패턴 (요청 당 3건의 로그):
요청 ID 08901761-ed2c-42e2-8c37-387d8a2a25b7 기준:
[2026-04-28T22:56:14.437Z] [INFO] Cupix::IspringService#create_user_account
"creating ispring account for user: mike.lee@unispace.com" (user_id=6062)
[2026-04-28T22:56:16.438Z] [ERROR] IspringOperation#create_user
"iSpring create user failed: 400 Bad Request"
[2026-04-28T22:56:16.438Z] [ERROR] Cupix::IspringService#create_user_account
"iSpring account creation failed: 400 Bad Request" (error.msg="400 Bad Request", user_id=6062)
영향 받은 사용자:
| User ID | 시도 횟수 | |
|---|---|---|
| 6062 | mike.lee@unispace.com | ~10 |
| 6063 | jason.bussas@unispace.com | ~6 |
주요 관찰:
- 100% 실패율: 해당 시간대에 iSpring 사용자 생성 성공 건수가 0건
- 반복 재시도: 동일 사용자에 대해 약 2초 간격으로 반복 호출 — 클라이언트 측 자동 재시도 또는 사용자의 반복 클릭 추정
- 2개 호스트에서 동시 발생:
ip-10-1-83-50과ip-10-1-16-74모두에서 에러 발생 - 응답 본문 미기록: iSpring API의 상세 거부 사유를 확인할 수 없음
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | iSpring 측에 이미 해당 이메일로 사용자가 존재하여 중복 생성 거부 | 로컬 DB 중복 검사(ARG30003)가 iSpring API 상태를 확인하지 않음 (ispring_service.rb:10-11). 두 사용자 모두 Unispace 조직으로 다른 경로로 이미 등록되었을 가능성. 100% 실패율이 일관됨 |
응답 본문이 로깅되지 않아 iSpring 측 에러 메시지 직접 확인 불가 | Confirmed (most likely) |
| H2 | iSpring API 인증 토큰 만료 또는 권한 부족 | — | 인증 실패 시 401 Unauthorized 또는 403 Forbidden이 반환되어야 하지만, 실제로는 400 Bad Request가 반환됨. get_access_token에서 에러가 발생했다면 별도의 에러 메시지가 기록되었을 것 |
Rejected |
| H3 | 요청 본문의 필수 필드 누락 또는 잘못된 형식 (예: firstname/lastname가 nil) |
user.firstname 또는 user.lastname가 nil인 경우 iSpring API가 400을 반환할 수 있음 |
Unispace 조직의 사용자가 이름/성이 없을 가능성은 낮음. 또한 두 사용자 모두 동일하게 실패하는 것은 필드 누락보다 구조적 원인을 시사 | Inconclusive — 응답 본문 로깅 부재로 확인 불가 |
| H4 | iSpring API 일시적 장애 또는 rate limit | 38초간 16회 호출은 rate limit 트리거 가능 | Rate limit 초과 시 보통 429 Too Many Requests가 반환됨. 또한 첫 번째 요청부터 실패하므로 rate limit이 근본 원인은 아님 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
app/operations/ispring_operation.rb:36-42—create_user메서드의 에러 핸들링을update_user_role(line 70-72) 및create_department(line 110-112)와 동일한 패턴으로 수정:RestClient::Exception을 먼저 catch하여e.response.body를 로깅에 포함- 이를 통해 iSpring API가 반환하는 정확한 거부 사유(중복 이메일, 유효하지 않은 필드 등)를 파악 가능
단기 개선 (1주 이내)#
app/services/cupix/ispring_service.rb:10-11—ARG30003중복 검사를 강화하여, 로컬 DB뿐 아니라 iSpring API의 사용자 조회 엔드포인트를 호출하여 이미 등록된 사용자인지 사전 확인하는 로직 추가 검토- 클라이언트(프론트엔드) 측에서 생성 실패 시 반복 재시도하는 로직이 있는지 확인하여, 불필요한 반복 호출 방지
장기 개선 (재발 방지)#
- iSpring 연동 로직 전반에 걸쳐 에러 핸들링 일관성 확보: 모든 external API 호출에서
RestClient::Exception을 별도로 catch하고 응답 본문 + 요청 본문을 로깅하는 패턴 통일 - iSpring 사용자 동기화 메커니즘 도입 검토: 로컬 DB와 iSpring 측 상태 불일치를 주기적으로 감지하고 복구하는 배치 작업
Monitoring#
- iSpring API 호출 실패율 모니터링 추가:
service:cupixworks-api "iSpring create user failed" status:error
- 동일 사용자에 대한 반복 실패 알림 (5분 내 동일 user_id로 3회 이상 실패 시):
service:cupixworks-api "iSpring account creation failed" @usr.id:* status:error
Risk Assessment#
- Risk level: medium — 사용자 onboarding이 차단되지만, 핵심 서비스(캡처, 3D 처리)에는 영향 없음
- 예상 복잡도: trivial —
create_user메서드의 rescue 절에RestClient::Exceptioncatch 추가는 기존 코드 패턴(update_user_role)을 따르면 됨