HTTP request failed
RCA: HTTP request failed (check_tile_uploading 400)
Overview#
What Happened#
cupix-tesla-floorplan-agent (Datadog service: cupixworks-any-floorplan-agent)는 floorplan 이미지를 타일링한 뒤 S3 에 업로드하고 Rails API의 PUT /api/v1/floorplans/:id/check_tile_uploading 으로 검증한다. 2026-06-24 KST 기준 chrischoi 테넌트에서 9건의 호출이 HTTP 400 (STAT10000 / Cupix::Errors::InvalidState — No tile objects found in S3)으로 실패했고, 이 응답을 SDK가 일괄적으로 HttpError: HTTP request failed 로 표면화시켜 동일 메시지의 9건 클러스터로 잡혔다. 동일 floorplan(예: 89189, 89195, 89262)에 대해 동일 시간대에 200 (성공) 응답도 관측되어 있어, 사용자가 같은 floorplan을 재처리한 일부 시도에서 타일이 업로드되지 않은 채 검증 단계에 도달한 패턴이다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | HttpError |
| exception.message | HTTP request failed |
| top_frame | node_modules/@tesla/typescript-node-sdk/api/floorplanApi.js:364:40 |
| upstream.status | 400 Bad Request |
| upstream.code | STAT10000 (Cupix::Errors::InvalidState) |
| upstream.message | No tile objects found in S3 |
| upstream.endpoint | PUT /api/v1/floorplans/:id/check_tile_uploading |
| env | production / us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
chrischoi (tenant cupix) |
9 | 동일 사용자(yohan.kim@cupix.com, user_id=5874)가 처리한 floorplan(89189, 89195, 89198, 89199, 89262, 89188 등)의 일부 재처리 시도에서 타일이 검증되지 않아 floorplan 상태 전이가 막힘. 사용자 화면에서는 floorplan 처리 실패로 보임. 다른 테넌트로의 영향은 관측되지 않음. |
Timeline#
- 2026-06-24 14:02 KST — Floorplan 89189 의 첫 번째 처리 사이클에서
check_tile_uploading200 성공 (floorplan_done메일 송신). (Datadog: cupixworks-api) - 2026-06-24 14:16 KST — Cluster
first_seen. floorplan 89188 처리에서HTTP request failed첫 발생. agent 는cleanUpAnythingRelatedModel→ SQSdeleteMessage흐름으로 실패 메시지를 삭제하고 종료. - 2026-06-24 15:29 KST — 동일 floorplan 89189 재처리 시
[400] PUT /api/v1/floorplans/89189/check_tile_uploading(STAT10000 — No tile objects found in S3). - 2026-06-24 15:35 KST — 89195 동일 패턴으로 400 실패.
- 2026-06-24 19:01 KST — 89262
check_tile_uploading200 성공. - 2026-06-24 19:54 KST — Cluster
last_seen. 89262 의 후속 재처리에서 다시 400 실패 (총 9건).
Error Log#
HTTP request failed
Full stack from agent (Datadog raw log):
HttpError: HTTP request failed
at Request._callback (/tmp/agent/dist/node_modules/.pnpm/@tesla+typescript-node-sdk@1.13.3-SNAPSHOT.../node_modules/@tesla/typescript-node-sdk/api/floorplanApi.js:364:40)
at self.callback (/tmp/agent/.../request@2.88.2/node_modules/request/request.js:185:22)
at Request.emit (node:events:524:28)
...
Upstream Rails response body that the SDK converted into the generic HttpError:
{
"result": {
"reason": "No tile objects found in S3",
"code": "STAT10000",
"type": "Cupix::Errors::InvalidState",
"message": "No tile objects found in S3"
}
}
Impact#
- Service:
cupixworks-any-floorplan-agent(repo:cupixworks/applications/agents/packages/cupix-tesla-floorplan-agent) - Team: chrischoi
- 발생 횟수: 9
- 최초 발생: 2026-06-24 14:16 KST
- 최근 발생: 2026-06-24 19:54 KST
Root Cause Summary#
Rails 측 check_tile_uploading! (app/models/concerns/tile/s3.rb:9-15) 는 tile_object_key_base(ver: tile_upload_revision) prefix 로 S3 객체 수를 세어 0이면 Cupix::Errors::InvalidState (STAT10000) 을 raise 한다. 즉 400 응답은 "agent 가 호출한 시점에 S3 의 해당 prefix 에 타일 객체가 하나도 없음"을 정확히 보고한 정상 동작이다. 문제는 agent 측에 있다 — aws-s3.manager.ts 의 uploadDirectoryToS3 가 chunk 내 개별 파일 업로드 실패를 try/catch 로 잡아 logger.error 만 찍고 throw 하지 않으며 (line 155-157), 또한 targetDirectoryPath 에 파일이 0개여도 그대로 정상 종료한다. 그 직후 floorplan-service.ts:357 에서 checkTileUploading 을 호출하기 때문에, 업로드가 부분적/완전 실패한 상태에서도 검증 API 가 호출되어 400 이 발생한다. 거기에 더해 floorplan-service.ts:106-108 의 광범위한 catch (err) { logger.error(...) } 가 에러를 삼키고 runByMessage 가 정상 종료 흐름을 타기 때문에 SQS 메시지가 deleteMessage 로 제거되고, 이후 사용자가 동일 floorplan 을 재처리할 때까지 자동 복구되지 않는다.
Technical Analysis#
Code Path#
Agent 진입 → 타일 생성 → S3 업로드 → 검증 호출 흐름:
try {
const exifTags = await this.getExifTags(cpFloorplan.localFilePath as string);
await this.updateFloorplanExif(targetId, exifTags);
const resolution = await this.getImageResolution(cpFloorplan.localFilePath);
await this.updateFloorplanResolution(targetId, resolution);
await this.tileFloorplan(cpFloorplan);
await this.uploadTile(cpFloorplan);
} catch (err) {
logger.error('FloorplanService::run | error', err);
}
기대 동작은 uploadTile 이 실패하면 메시지 처리를 중단하고 SQS retry 가 일어나는 것이지만, 위 catch 가 모든 예외를 삼키기 때문에 runByMessage (base-service.ts:153-189) 는 정상 경로로 계속 진행하여 cleanUpAnythingRelatedModel → deleteMessage 를 실행한다.
uploadTile 본체는 S3 업로드 직후 검증 API 를 부른다:
await awsS3Manager.uploadDirectoryToS3({
bucketName: s3Credentials.bucket_name,
bucketKeyPath: s3Credentials.basepath,
targetDirectoryPath: resultDir,
acl: s3Credentials.acl,
excludeFileExtension: true
});
await this.cupixApi.floorplan.checkTileUploading(cpFloorplan.id);
uploadDirectoryToS3 의 chunk-level 에러 핸들링이 핵심 결함:
const uploadChunk = async (filesChunk: string[]) => {
await Promise.all(filesChunk.map(async (filePath) => {
try {
await this.checkToken();
if (this.s3 == undefined) {
logger.error('AwsS3Manager::uploadDirectoryToS3 | s3 is not initialized');
return;
}
// ...
await this.s3.upload({ /* ... */ }).promise();
uploadedFileCount++;
} catch (error) {
logger.error('AwsS3Manager::uploadDirectoryToS3 | failed: %s', filePath, error);
// 에러 swallow — throw/reject 하지 않음
}
}));
};
// 함수 종료 시 uploadedFileCount === 0 이거나 filesToUpload.length === 0 인 경우에 대한 검증 없음
기대 동작: 업로드 실패가 누적되거나 파일이 0개면 caller 에 예외를 전달하여 checkTileUploading 호출을 막아야 한다. 실제 동작: 모든 실패가 silent log 로 종료되고, 함수는 정상 resolve.
Rails 측 검증 로직 (정상 동작, 단지 메시지가 일반화되어 클러스터에 묶임):
def check_tile_uploading!
objects_count = tile_uploading_objects.size
self.tile_size = objects_count if self.has_attribute?(:tile_size)
raise Cupix::Errors::InvalidState.new(code: 'STAT10000', reason: 'No tile objects found in S3') if objects_count.zero?
self.uploaded_tile_state!
end
def tile_object_key_base(opts = {})
return self.sys[:original_tile_object_key_base] if self.sys[:original_tile_object_key_base].present?
_content_url_type = opts[:content_url_type] || content_url_type
ver = opts[:ver].presence || self.tile_revision
case _content_url_type
when 'public'
"#{tile_hex(ver)}/floorplan_tiles/#{bucket_region_code}/#{self.id.to_s(36)}"
# ...
end
end
tile_upload_revision = tile_revision + 1 (tile.rb:10-12). Agent 가 tile_upload_credentials 로 받은 prefix 와 Rails 가 검증할 때 사용하는 prefix 는 동일한 ver 로 계산되므로, prefix mismatch 가 아닌 "이 prefix 에 객체가 0개" 가 정확한 사실이다 (S3 list 응답은 동일 region 내 strong read-after-write 일관성을 가지므로 timing race 도 배제됨).
Log Evidence#
검색 1: 클러스터의 원본 에러 (Datadog)
service:cupixworks-any-floorplan-agent status:error "HTTP request failed"
agent 가 받은 응답 본문 (raw Datadog log):
{
"request": { "method": "PUT",
"uri": { "pathname": "/api/v1/floorplans/89262/check_tile_uploading", "host": "api-tesla.cupix.internal" } },
"response": {
"statusCode": 400,
"body": { "result": {
"reason": "No tile objects found in S3",
"code": "STAT10000",
"type": "Cupix::Errors::InvalidState",
"message": "No tile objects found in S3"
} }
},
"team": { "domain": "chrischoi", "id": 871 },
"floorplan": { "id": 89262 },
"user": { "id": 5874, "email": "yohan.kim@cupix.com" },
"@timestamp": "2026-06-24T10:54:42.223Z"
}
검색 2: 실패 직후 agent 가 SQS 메시지를 삭제하고 종료한다는 증거
service:cupixworks-any-floorplan-agent (89189 OR 89195 OR 89262 OR 89198 OR 89199 OR 89188)
2026-06-24 14:53:40 info AwsQueueManager::deleteMessage | end - message id: 77f15031-...
2026-06-24 14:53:40 info AwsQueueManager::deleteMessage | begin - queue url: https://sqs.us-west-2.amazonaws.com/002596530511/cupix-tesla-floorplan-agent-production
2026-06-24 14:53:40 info BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/89198
2026-06-24 14:53:40 error HTTP request failed
검색 3: Rails 측에서 동일 floorplan 에 대해 200/400 이 교차 관측됨 (재처리 시 일부 사이클만 실패)
service:cupixworks-api 89189
2026-06-24 15:29:18 [400] PUT /api/v1/floorplans/89189/check_tile_uploading STAT10000 / No tile objects found in S3
2026-06-24 14:02:08 [200] PUT /api/v1/floorplans/89189/check_tile_uploading
2026-06-24 12:59:43 [200] POST /api/v1/floorplans/89189/tile_upload_credentials
2026-06-24 12:59:31 [200] PUT /api/v1/floorplans/89189
service:cupixworks-api 89262
2026-06-24 19:54:43 [400] PUT /api/v1/floorplans/89262/check_tile_uploading STAT10000
2026-06-24 19:01:07 [200] PUT /api/v1/floorplans/89262/check_tile_uploading
2026-06-24 18:18:33 [200] POST /api/v1/floorplans/89262/tile_upload_credentials
uploadDirectoryToS3 내부의 failed: %s 또는 file count debug 로그는 Datadog 에 보이지 않는다 (해당 로그들은 silly/debug level 이며 Datadog 는 error/warn/info 만 보존). 따라서 "정확히 어떤 파일이 어떤 사유로 업로드에 실패했는지"는 Datadog 만으로는 확정 불가 — uncertain, needs verification via Cupix Watch (Kibana) silly logs in the same time window.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Agent 의 uploadDirectoryToS3 가 일부/전체 파일 업로드 실패를 silent 로 무시하고 checkTileUploading 을 호출 → Rails 가 정확히 "객체 없음" 을 보고 (STAT10000) |
aws-s3.manager.ts:155-157 swallow catch; floorplan-service.ts:357 직후 검증 호출; Rails 응답 body STAT10000; 동일 floorplan 이 다른 시도에서 200 으로 성공 (재처리 시 정상 케이스 존재) |
uploadDirectoryToS3 내부 file count 와 실제 업로드 실패 사유가 silly/debug 로그라 Datadog 에서 미확보 — 결정적 fingerprint 는 uncertain |
Confirmed (root mechanism 확정, 실패 trigger 의 세부 사유는 Watch 확인 필요) |
| H2 | Rails 와 agent 가 서로 다른 tile_revision prefix 를 보아 prefix mismatch 로 0개로 보임 |
tile_upload_revision = tile_revision + 1 라는 +1 점프 존재 |
s3.rb:21 (prefix: tile_object_key_base(ver: tile_upload_revision)) 와 s3.rb:60 (key: tile_object_key_base(ver: tile_upload_revision)) 가 동일 식 사용 — agent 가 받은 credentials key 와 Rails 가 list 한 prefix 가 동일. 또한 동일 floorplan 동일 revision 에서 200 이 관측됨 → prefix 자체는 일관됨 |
Rejected |
| H3 | S3 read-after-write 일관성 지연으로 list 가 0건 반환 | 일부 floorplan 은 재처리 후 200 → 시점에 따라 결과가 달라짐 | us-west-2 S3 는 2020-12 이후 모든 PUT/LIST 에 대해 strong read-after-write consistency 보장. agent 의 s3.upload(...).promise() 가 await 되므로 응답 수신 후 list 결과는 즉시 일관 |
Rejected |
| H4 | SQS retry 폭주로 같은 메시지가 반복 처리되며 누적 실패 | 동일 floorplan 에서 다중 실패 발생 | 로그상 매 실패마다 AwsQueueManager::deleteMessage 가 동기적으로 호출되어 메시지가 즉시 제거됨 (base-service.ts:171-172 가 catch 내부에서가 아니라 정상 경로로 실행되기 때문) → 같은 메시지 재실행이 아니라 별개 user 트리거 메시지 |
Rejected |
| H5 | Rails 측 버그로 STAT10000 을 잘못 발생 |
— | tile/s3.rb:9-15 는 단순 count 후 raise. 동일 prefix 에서 200 도 발생하므로 검증 자체는 정상. 또한 STAT10000 을 raise 한 시점에 기록한 Rails info 로그([400] PUT ... check_tile_uploading)는 Rails 응답이 정확하다는 추가 증거 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
aws-s3.manager.ts:130-167—uploadDirectoryToS3의 chunk-level catch 에서 에러를 throw 하도록 변경하거나, 최소한 함수 종료 시uploadedFileCount !== filesToUpload.length인 경우 throw. 또한filesToUpload.length === 0인 경우 명시적으로 throw (현재는 0개여도 정상 resolve). 이 한 곳만 막아도 검증 API 호출 자체가 발생하지 않으므로 사용자에게 보이는 에러가 의미 있는 메시지(예: "tile upload failed: N/M files")로 바뀐다.floorplan-service.ts:309-358(uploadTile) —awsS3Manager.uploadDirectoryToS3호출 직후 (checkTileUploading호출 전에) 업로드된 파일 수를 명시적으로 확인하고 0 또는 부분 실패면 즉시 throw. 단기 방어 라인.
단기 개선 (1주 이내)#
floorplan-service.ts:98-108의 광범위한catch (err) { logger.error(...) }를 좁힌다. 현재는 모든 예외를 삼키기 때문에runByMessage가 정상 종료 흐름으로 진입해 SQS 메시지가 즉시 삭제된다. 결과적으로 사용자 수동 재처리 외에는 자동 복구가 불가능하다. 실패 시 catch 를 제거하거나 내부에서 다시 throw 하여base-service.ts:182-185의 catch 에 도달시킨 뒤 SQS 의 visibility timeout / DLQ 로 retry 또는 격리되도록 한다.uploadDirectoryToS3의 silly 로그(파일 단위 업로드 결과)를 info 또는 warn 레벨로 승격하여 Datadog 만으로도 이런 부분 실패를 즉시 진단할 수 있게 한다.- Rails 응답 본문(
code,reason)을 agent 측 logger 가 error 메시지에 그대로 합성하도록 변경 (현재는HttpError: HTTP request failed으로 정보가 손실되어 클러스터링 시 9개의 서로 다른 사용자 시나리오가 한 클러스터에 합쳐짐).
장기 개선 (재발 방지)#
- 타일 업로드 파이프라인을 "upload → list → diff → retry" 로 재설계: 업로드 직후 agent 가 자체적으로 S3 list 를 호출하여 기대 파일 수와 일치하는지 검증하고, 일치하지 않으면 누락 파일만 재업로드 후 검증 API 를 호출.
check_tile_uploading응답이 400 일 때 SDK 가 throw 하는 genericHttpError를 도메인 에러 (예:TileMissingError) 로 변환하여 운영자가 dashboard 에서 즉시 식별 가능하도록 한다.- 모든 agent 의 base run 루프에서 "에러 발생 시 SQS deleteMessage 를 건너뛰고 visibility timeout 만료를 기다리는" 정책으로 통일 (현재
runByMessage내부에서 catch 를 만나면 throw 가 일어나야 deleteMessage 가 스킵되는데, 서비스별run함수가 자체 catch 로 에러를 먹으면 그 보호가 무력화됨).
Monitoring#
타일 업로드 실패 자체를 직접 카운트:
sum:trace.express.request.errors{service:cupixworks-api,resource_name:put_/api/v1/floorplans/*/check_tile_uploading}.as_count()
(상기 trace 메트릭이 활성화되지 않은 경우 fallback — log-based count via Log Explorer query embedded as a saved view; widget 에는 metric 사용 권장)
Agent 측 generic HTTP request failed 카운트 (재발 시 즉시 알람):
sum:logs.hits{service:cupixworks-any-floorplan-agent,status:error,@error.message:"HTTP request failed"}.as_count()
(만약 logs.hits custom metric 이 없으면 Datadog Logs → Generate Metric 으로 service:cupixworks-any-floorplan-agent status:error "HTTP request failed" 를 사전 생성한 뒤 위 메트릭 이름을 사용한다.)
부분 실패를 조기에 잡기 위한 업로드 성공률 메트릭 (장기 개선과 함께 추가):
sum:agents.s3.upload.files_uploaded{service:cupix-tesla-floorplan-agent}.as_count() / sum:agents.s3.upload.files_total{service:cupix-tesla-floorplan-agent}.as_count()
(files_uploaded / files_total 두 카운터를 aws-s3.manager.ts 에서 emit 한 후에만 의미 있음 — 단기 개선 후에 dashboard 에 반영)
Datadog 대시보드 timeseries widget 에 위 쿼리를 그대로 넣을 때
| stats,count by(...), threshold suffix 등을 사용하지 말 것 (monitor-only 문법 — widget 이 빈 그래프로 렌더링됨).
Risk Assessment#
- Risk level: medium — 사용자 가시 에러이고 재처리로 우회 가능하지만, 동일 사용자(yohan.kim) 가 반복적으로 영향을 받았고
HTTP request failed라는 generic 메시지로 인해 디버깅이 어려움. 다른 테넌트로의 직접 영향은 미관측. - 예상 복잡도: standard —
aws-s3.manager.ts의 catch 동작과floorplan-service.ts의 catch 범위를 좁히는 변경이 핵심. 두 파일 모두 spec 이 존재하므로 회귀 위험 낮음. 검증은cupix-tesla-floorplan-agent+base패키지의 기존 vitest 스위트 + 수동 재현 1건이면 충분.