FloorplanService::uploadFile | response path: /tmp/workspace/86196/86196.png, code: 403, message: Fo
RCA: FloorplanService::uploadFile | 403 Forbidden
Overview#
What Happened#
2026-05-07 22:47~23:05 UTC, cupixworks-any-floorplan-agent 서비스에서 floorplan 파일을 S3에 업로드하는 과정에서 HTTP 403 Forbidden 에러가 2건 발생했다. 동일 사용자(philip.hall@cupix.com)가 업로드한 고해상도 PDF 도면(LOD 7, 32768x32768 pixels) 처리 중 presigned URL이 만료되어 S3 PUT 요청이 거부된 것이 원인이다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | HTTP 403 (S3 Presigned URL Rejected) |
| exception.message | FloorplanService::uploadFile | response path: /tmp/workspace/86196/86196.png, code: 403, message: Forbidden |
| top_frame | floorplan-service.ts:297 |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| redwoodeg (Redwood Electric Group) | 2 | 고해상도 PDF 도면 2건 처리 실패, 사용자 Floorplan 사용 불가 |
Timeline#
- 2026-05-07T21:02:58Z — Floorplan 86214 생성 (user: philip.hall@cupix.com)
- 2026-05-07T20:45:40Z — Floorplan 86196 첫 번째 처리 시도 시작
- 2026-05-07T21:03:57Z — Floorplan 86214 처리 시작 (presigned URL 발급)
- 2026-05-07T22:46:31Z — Floorplan 86196 두 번째 처리 시도 (SQS 재전송)
- 2026-05-07T22:47:27Z — Floorplan 86196 uploadFile 403 실패
- 2026-05-07T23:05:01Z — Floorplan 86214 uploadFile 403 실패 (presigned URL TTL 2h 초과)
Error Log#
FloorplanService::uploadFile | response path: /tmp/workspace/86196/86196.png, code: 403, message: Forbidden
Impact#
- Service:
cupixworks-any-floorplan-agent - 발생 횟수: 2
- 최초 발생: 2026-05-07T22:47:27.334Z
- 최근 발생: 2026-05-07T23:05:01.811Z
- 영향 받은 floorplan: 86196, 86214 (동일 사용자, 동일 세션)
- 사용자 영향: 도면 처리 실패 → processing_failed 상태로 전환, Slack 알림 발송됨
Root Cause Summary#
Floorplan agent가 Tesla API에서 presigned S3 upload URL을 발급받은 후, GraphicsMagick을 이용한 PDF → PNG 변환(translateFloorplan)이 2시간 이상 소요되어 presigned URL의 TTL(2시간)을 초과했다. LOD 7(32768x32768 pixels, 300 DPI)의 대형 PDF 도면 변환은 극도로 높은 연산 부하를 유발하며, 현재 presigned URL은 변환 완료 전에 만료된다.
Technical Analysis#
Code Path#
- Entry point:
floorplan-service.ts:51(runmethod) - Presigned URL 획득:
cpfloorplan.ts:62(API 응답에서upload_url저장) - 변환 시작:
floorplan-service.ts:81(translateFloorplan) - 업로드 시도:
floorplan-service.ts:82(uploadFile) - Failure point:
floorplan-service.ts:296-297(S3 응답 403)
1. Presigned URL 발급 (Tesla API)
def presigned_upload_url(revision, force: false)
revision ||= self.revision + 1
if !force && revision < self.revision
raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: "Invalid revision: #{revision}")
end
client = Cupix::StorageService.client(storage_option: storage_option)
signer = Aws::S3::Presigner.new(client: client)
bucket_name = storage_option.s3_source_bucket_name
expires_in = 2.hour.to_i # ← 7200초 = 2시간 TTL
signer.presigned_url(
:put_object,
bucket: bucket_name,
key: object(revision).key,
storage_class: 'ONEZONE_IA',
expires_in: expires_in,
acl: 'bucket-owner-full-control'
)
end
2. 실행 흐름 (Floorplan Agent)
if (cpFloorplan.uploadUrl != undefined
&& cpFloorplan.localFilePath != undefined
&& cpFloorplan.cpFloorplanSource.downloadUrl != undefined
&& cpFloorplan.cpFloorplanSource.originalFilePath != undefined
) {
await this.updateFloorplanState(targetId, TESLA.FloorplanState.Processing);
await this.downloadFileWithHeader(
cpFloorplan.cpFloorplanSource.downloadUrl,
cpFloorplan.cpFloorplanSource.originalFilePath,
cpFloorplan.cpFloorplanSource.headers
);
await this.translateFloorplan(cpFloorplan); // ← LOD 7에서 2시간+ 소요
await this.uploadFile(cpFloorplan.uploadUrl, cpFloorplan.localFilePath); // ← 이 시점에 URL 만료
await this.cupixApi.floorplan.checkUploading(cpFloorplan.id);
}
기대 동작: translateFloorplan 완료 후 presigned URL로 S3 업로드 성공
실제 동작: 변환이 2시간 이상 소요 → presigned URL 만료 → S3가 403 반환
3. uploadFile 메서드 (실패 지점)
private uploadFile = (url: string, path: string, headers?: any): Promise<void> => new Promise((resolve, reject) => {
this.setErrorCode(ErrorCode.Agent.FloorplanFailedToUpload);
logger.debug('FloorplanService::uploadFile | start path: %s, url: %s', path, url);
const cupixAuth = this.cupixAuth;
const sendReq = request.put(url, {
headers: headers != undefined ? headers : {
'X-CUPIX-AUTH': cupixAuth.accessToken
},
body: fs.readFileSync(path)
});
sendReq
.on('response', res => {
if (res.statusCode !== 200) {
logger.error('FloorplanService::uploadFile | response path: %s, code: %d, message: %s', path, res.statusCode, res.statusMessage);
cupixAuth.handleError(res);
reject(); // ← 403 시 reject, 에러 정보 없이 reject
}
})
});
4. 변환 파라미터 (LOD 7)
const tileLod = this.determineTileLod(cpFloorplan);
const width = Constants.DefaultTilePixel * Math.pow(2, tileLod); // 256 * 2^7 = 32768
const height = Constants.DefaultTilePixel * Math.pow(2, tileLod); // 256 * 2^7 = 32768
const dpi = Constants.DefaultDpi; // 300
32768x32768 pixels = 약 1.07 billion pixels. GraphicsMagick으로 300 DPI PDF를 이 크기로 래스터라이즈하는 것은 메모리와 CPU 집약적 작업이다.
Log Evidence#
Datadog 검색 쿼리:
service:cupixworks-any-floorplan-agent "86214"
Floorplan 86214 타임라인 (presigned URL 만료 확인):
2026-05-08 06:03:57 KST [200] GET /api/v1/floorplans/86214 (presigned URL 발급 시점)
2026-05-08 06:03:57 KST [200] PUT /api/v1/floorplans/86214 (state → processing)
2026-05-08 06:03:57 KST FloorplanService::translateFloorplan | source: .../35021.pdf, width: 32768, height: 32768, dpi: 300, LOD: 7
2026-05-08 08:05:01 KST FloorplanService::uploadFile | response path: .../86214.png, code: 403, message: Forbidden
경과 시간: 06:03:57 → 08:05:01 = 2시간 1분 4초 (presigned URL TTL 2시간 초과)
Floorplan 86196 타임라인 (2번 시도):
# 첫 번째 시도
2026-05-08 05:45:40 KST BaseService::runByMessage | id: 86196
2026-05-08 05:45:42 KST FloorplanService::translateFloorplan | source: .../34993.pdf, width: 32768, height: 32768, dpi: 300, LOD: 7
# (약 2시간 후 presigned URL 만료로 실패 → SQS 재전송)
# 두 번째 시도
2026-05-08 07:46:31 KST BaseService::runByMessage | id: 86196
2026-05-08 07:46:33 KST FloorplanService::translateFloorplan | source: .../34993.pdf, width: 32768, height: 32768, dpi: 300, LOD: 7
2026-05-08 07:47:27 KST FloorplanService::uploadFile | response path: .../86196.png, code: 403, message: Forbidden
두 번째 시도는 변환 시간이 54초로 짧은데(캐시된 출력 파일 존재 가능), 여전히 403 실패. 이는 첫 번째 시도의 실패로 인해 resource state가 변경되어 유효한 presigned URL이 재발급되지 않았거나, S3 bucket policy와 presigned URL의 signed parameter 불일치가 원인일 수 있다.
에러 후 처리 결과 확인:
{
"event_id": 743652322,
"action": "update",
"reason": "processing_failed",
"model_type": "Floorplan",
"model_id": 86214
}
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Presigned URL TTL(2h) 초과로 URL 만료 | 86214: 06:03:57→08:05:01 = 2h1m4s > 2h TTL. 86196: 첫 시도도 ~2h 소요 | 86196 두 번째 시도는 54초 내 실패 (fresh URL이면 만료 아님) | Confirmed (primary) |
| H2 | S3 bucket ACL/ObjectOwnership 정책 변경 | 403은 ACL 관련 S3 거부 시 나타남 | Infrastructure: object_ownership = "ObjectWriter" (ACL 활성). resource.rb 최근 변경 없음 |
Rejected |
| H3 | Agent session token 만료로 presigned URL 발급 실패 | 2h 이상 실행 시 세션 만료 가능 | API GET 호출 성공 ([200] GET /api/v1/floorplans/86214), presigned URL은 API 서버 측 AWS credentials로 생성 |
Rejected |
| H4 | 대용량 파일(32768x32768 PNG) S3 single PUT 제한 초과 | S3 single PUT은 5GB 한도 | 32768x32768 PNG는 ~100-500MB 수준으로 5GB 미만 | Rejected |
| H5 | 두 번째 시도에서 resource_state 변경으로 nil upload_url 반환 | 첫 시도 실패 후 resource_state가 done 등으로 변경될 수 있음 |
코드 확인: updateErrorState는 floorplan state만 변경, resource_state는 여전히 uploading으로 serializer가 upload_url 반환함. 또한 nil이면 uploadFile 자체가 호출되지 않음 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
app/models/concerns/storagable/resource.rb:121— presigned URL TTL을 현재2.hour.to_i에서6.hours.to_i또는12.hours.to_i로 증가. S3 presigned URL 최대 TTL은 7일(IAM user) 또는 12시간(STS credentials)이므로 credential 유형 확인 필요.
단기 개선 (1주 이내)#
floorplan-service.ts:82—translateFloorplan완료 후uploadFile호출 직전에 Tesla API에서 fresh presigned URL을 재발급 받는 로직 추가. 변환 시간이 길어도 항상 유효한 URL 사용 보장.- 대안:
translateFloorplan완료 후cupixApi.floorplan.get(targetId)를 다시 호출하여 최신upload_url을 획득한 뒤uploadFile에 전달.
장기 개선 (재발 방지)#
- LOD 7 이상의 대형 도면에 대해
translateFloorplan타임아웃 또는 프로그레스 모니터링 도입. 변환 시간이 presigned URL TTL에 근접할 경우 사전 경고 또는 URL 갱신 로직 트리거. - GraphicsMagick 성능 최적화 또는 더 높은 사양의 인스턴스 타입 사용 검토 (현재 32768x32768 변환에 2시간+ 소요는 비정상적으로 느림).
- 파일 크기/해상도에 따른 적응형 presigned URL TTL 설정 고려.
Monitoring#
- 추가 메트릭:
translateFloorplan소요 시간을 측정하는 로그 또는 메트릭 추가 - Datadog 알림: 변환 시간이 1시간 초과 시 경고
- 쿼리 예시:
service:cupixworks-any-floorplan-agent "FloorplanService::uploadFile" status:error
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard
- 영향 범위: LOD 7(또는 그 이상) 크기의 대형 PDF 도면에만 해당. 일반적인 도면(LOD 5-6)은 변환 시간이 presigned URL TTL 내에 완료되므로 영향 없음.