ES /docs

Expected positive integer for bottom but received -96409 of type number

RCA: Expected positive integer for bottom but received -96409 of type number

Overview#

What Happened#

2026-05-26 13:24:47 UTC에 cupixworks-any-floorplan-agent 서비스에서 floorplan 87012를 타일링하는 과정에서 sharp 라이브러리의 extend() 호출 시 bottom 파라미터에 음수 값(-96409)이 전달되어 에러가 발생했다. 이미지의 실제 높이가 maximumPixels (65,536)보다 커서 extend 계산 결과가 음수가 된 것이 원인이다.

Quick Facts#

Field Value
exception.class Error
exception.message Expected positive integer for bottom but received -96409 of type number
top_frame sharp/lib/resize.js:412FloorplanService.tileFloorplan (app.cjs:6882)
runtime Node.js, sharp@0.33.5
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
aec-on (team 657) 1 Floorplan 87012 타일링 실패, 사용자(mvillamor@aec-on.com)의 floorplan 처리 완료 불가

Timeline#

  1. 13:24:18 UTC — 세션 시작, floorplan 87012 처리 개시 (state: resource_uploaded)
  2. 13:24:18 ~ 13:24:47 UTC — 이미지 다운로드 및 translate 단계 수행
  3. 13:24:47.716 UTCtileFloorplan 시작
  4. 13:24:47.729 UTC — sharp extend() 호출 시 음수 bottom 값으로 에러 발생
  5. 13:24:47.730 UTC — workspace 정리 후 SQS 메시지 삭제 (재시도 없이 실패 처리)

Error Log#

Datadog Logs

text
Expected positive integer for bottom but received -96409 of type number

Impact#

  • Service: cupixworks-any-floorplan-agent
  • Team: aec-on
  • 발생 횟수: 1
  • 최초 발생: 2026-05-26T13:24:47.729Z
  • 최근 발생: 2026-05-26T13:24:47.729Z

Root Cause Summary#

FloorplanService.tileFloorplan 메서드에서 이미지를 타일링하기 전 sharp의 extend()를 호출하여 이미지를 정사각형으로 패딩하는데, 이때 bottom: maximumPixels - originalHeight 계산에서 originalHeight(161,945px)가 maximumPixels(65,536px)보다 커서 음수(-96,409)가 발생했다. getTileLodFromImage()가 LOD를 MaxTileLODForImage = 8로 캡하여 maximumPixels를 65,536으로 설정하지만, 실제 이미지가 이보다 클 경우에 대한 방어 로직이 없다.

Technical Analysis#

Code Path#

  • Entry point: floorplan-service.ts:51FloorplanService.run()
  • floorplan-service.ts:81translateFloorplan() 호출. determineTileLod()가 LOD를 최대 Highest = 7로 계산하고, GM에 32,768x32,768으로 resize 요청.
  • floorplan-service.ts:104tileFloorplan() 호출. 실제 translated 이미지 파일을 읽어 타일링 시작.
  • floorplan-service.ts:203getTileLodFromImage() 호출. 실제 이미지 dimensions에서 LOD 계산 후 MaxTileLODForImage = 8로 cap.
  • Failure point: floorplan-service.ts:214extend({ bottom: maximumPixels - originalHeight }) 호출 시 음수 값 전달.
floorplan-service.ts:203-215typescript
const tileLOD = await this.getTileLodFromImage(source, 0);
const maximumPixels = Constants.DefaultTilePixel * Math.pow(2, tileLOD);
// tileLOD는 MaxTileLODForImage=8로 cap → maximumPixels = 65,536
// 그러나 originalHeight가 161,945인 경우...
return new Promise((resolve, reject) => {
    sharp(source, sharpOptions)
        .png({ force: true })
        .extend({
            right: maximumPixels - originalWidth,
            bottom: maximumPixels - originalHeight,  // 65,536 - 161,945 = -96,409 ❌
            background: { r: 0, g: 0, b: 0, alpha: 0 }
        })

getTileLodFromImage()에서 LOD를 cap하는 로직:

floorplan-service.ts:421-436typescript
private getTileLodFromImage = async (filePath: string, additionalLevel?: number): Promise<number> => {
    const _additionalLevel = additionalLevel == undefined ? 0 : additionalLevel;
    const resolution = await this.getImageResolution(filePath);
    const width = resolution[0];
    const height = resolution[1];
    const _tileLOD = Math.ceil(Math.log2(Math.max(width, height) / Constants.DefaultTilePixel));

    if (Constants.MaxTileLODForImage > 0
        && _tileLOD + _additionalLevel > Constants.MaxTileLODForImage
    ) {
        return Constants.MaxTileLODForImage;  // 8로 cap하지만 실제 이미지는 더 클 수 있음
    } else {
        return _tileLOD + _additionalLevel;
    }
};

translateFloorplan()의 resize 설정:

floorplan-service.ts:161-163typescript
const tileLod = this.determineTileLod(cpFloorplan);
const width = Constants.DefaultTilePixel * Math.pow(2, tileLod);
const height = Constants.DefaultTilePixel * Math.pow(2, tileLod);

determineTileLod()의 cap 로직 — 최대 Highest = 7 (32,768px):

floorplan-service.ts:412-414typescript
const tileLODfromResolution = Math.ceil(Math.log2(Math.max(width, height) / Constants.DefaultTilePixel));
if (tileLODfromResolution > tileLOD) {
    tileLOD = tileLODfromResolution > Constants.FloorplanTileLOD.Highest ? Constants.FloorplanTileLOD.Highest : tileLODfromResolution;
}

GraphicsMagick resize 명령:

process/graphics-magick.process.ts:56-58typescript
if (width != undefined && height != undefined) {
    command += ` -resize ${width}x${height}`;
}

기대 동작: translateFloorplan()이 이미지를 최대 32,768x32,768로 resize한 후 tileFloorplan()이 해당 파일을 읽으면, getTileLodFromImage()는 LOD 7을 반환하고 maximumPixels = 32,768이 되어 extend가 정상 동작해야 한다.

실제 동작: translated 이미지의 높이가 161,945px로 65,536px을 초과했다. GraphicsMagick의 -resize WxH는 aspect ratio를 유지하면서 bounding box에 맞추므로 정상적으로는 32,768을 초과할 수 없다. 이미지가 예상대로 resize되지 않은 경우가 발생한 것으로, GraphicsMagick의 -density 300 플래그가 특정 이미지 포맷(TIFF 등)에서 DPI 해석에 영향을 주어 resize 결과가 의도한 크기를 초과했을 가능성이 있다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-any-floorplan-agent status:error @environment:production "Expected positive integer"
text
service:cupixworks-any-floorplan-agent @environment:production

에러 발생 전후 타임라인 (floorplan 87012):

text
13:24:18.962 [info] CupixAuth::setSession | session_id: 37aba3fd7797003e030ad2a7f63daedb9a9ace78
13:24:18.996 [info] FloorplanService::run | floorplan state: resource_uploaded, resource_state: uploaded
13:24:47.716 [info] FloorplanService::tileFloorplan | begin
13:24:47.729 [error] Expected positive integer for bottom but received -96409 of type number
13:24:47.730 [info] BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/87012
13:24:47.894 [info] AwsQueueManager::deleteMessage | begin
13:24:47.922 [info] AwsQueueManager::deleteMessage | end - message id: 7b7dbb04-8fd2-4785-90de-37b04a43ad97

에러 stack trace:

text
Error: Expected positive integer for bottom but received -96409 of type number
    at Object.invalidParameterError (/tmp/agent/dist/node_modules/.pnpm/sharp@0.33.5/node_modules/sharp/lib/is.js:135:10)
    at Sharp.extend (/tmp/agent/dist/node_modules/.pnpm/sharp@0.33.5/node_modules/sharp/lib/resize.js:412:18)
    at /tmp/agent/dist/app.cjs:6882:12
    at new Promise (<anonymous>)
    at FloorplanService.tileFloorplan (/tmp/agent/dist/app.cjs:6879:14)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async FloorplanService.run (/tmp/agent/dist/app.cjs:6775:11)

주요 식별 정보:

  • Floorplan ID: 87012
  • Team: aec-on (ID: 657)
  • User: mvillamor@aec-on.com (ID: 9941)
  • SQS Queue: cupix-tesla-floorplan-agent-production
  • Host: ip-10-1-46-206.us-west-2.compute.internal

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 tileFloorplan에서 extend 호출 시 이미지 크기가 maximumPixels를 초과하여 음수 패딩 값 계산 에러 메시지 "bottom = -96409" → originalHeight = 161,945 > maximumPixels(65,536). stack trace가 Sharp.extendFloorplanService.tileFloorplan 경로 확인. Confirmed
H2 GraphicsMagick translate 단계에서 resize가 정상 작동하지 않아 출력 이미지가 의도한 크기보다 훨씬 큼 세션 시작~tileFloorplan 사이 29초 갭은 translate 처리 시간. -density 300이 특정 이미지 포맷에서 resize 결과에 영향을 줄 수 있음. GM의 -resize WxH는 일반적으로 bounding box 내로 constraining함. translate 에러 로그 없음. Inconclusive
H3 tileFloorplan이 translated 파일이 아닌 원본 다운로드 파일을 읽음 run() 메서드 lines 89-96에서 localFilePath가 없으면 downloadUrl에서 재다운로드하는 로직 존재. translateFloorplan의 output이 cpFloorplan.localFilePath로 설정되므로 파일이 이미 존재해야 함. fs.existsSync 체크로 보호됨. Rejected
H4 getTileLodFromImage의 LOD cap(8)과 determineTileLod의 LOD cap(7) 불일치가 근본 원인 determineTileLod는 max 7 (32,768), getTileLodFromImage는 max 8 (65,536). 두 값이 다르면 extend 계산에서 불일치 발생 가능. 그러나 이 경우에도 extend는 양수여야 함 (32,768 이미지 → LOD 7 → maximumPixels 32,768). LOD cap 불일치 자체는 에러의 직접 원인이 아님. 이미지가 cap된 maximumPixels보다 클 때만 문제 발생. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: applications/agents/packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:212-215
  • extend() 호출 전에 rightbottom 값이 음수인지 검증하고, 음수일 경우 sharp의 resize()를 먼저 호출하여 이미지를 maximumPixels 이내로 축소한 후 extend를 적용해야 한다.
  • 또는 Math.max(0, maximumPixels - originalWidth) 등으로 음수 패딩을 방지하고, 이미지가 maximumPixels보다 클 경우 먼저 resize하는 분기를 추가.

단기 개선 (1주 이내)#

  • tileFloorplan에서 getTileLodFromImage 결과를 사용할 때, 실제 이미지 dimensions가 maximumPixels를 초과하는 경우를 명시적으로 처리하는 로직 추가. 이미지를 maximumPixels 크기로 resize한 후 extend+tile 진행.
  • translateFloorplan 이후 출력 파일의 dimensions를 검증하는 로그 추가 (info 레벨). translate 결과가 예상 크기를 초과하는 경우 warn 로그 출력.

장기 개선 (재발 방지)#

  • determineTileLod(translate용)와 getTileLodFromImage(tile용)의 LOD cap 값을 통일하거나, tile 단계에서 항상 이미지를 maximumPixels 이내로 보장하는 resize-then-extend 패턴 적용.
  • GraphicsMagick의 -density 플래그가 다양한 이미지 포맷에서 resize 결과에 미치는 영향을 테스트하고, 필요 시 raster 이미지에 대해 -density를 제외하거나 resize 결과를 검증하는 로직 추가.

Monitoring#

  • 추가할 메트릭: floorplan.tile.extend_negative_value — extend 계산 결과가 음수인 경우 카운트
  • Datadog 알림 쿼리:
text
service:cupixworks-any-floorplan-agent status:error "Expected positive integer for" @environment:production
  • translate 출력 이미지 크기 로그 추가 후 모니터링:
text
service:cupixworks-any-floorplan-agent "FloorplanService::tileFloorplan" "maximumPixels" @environment:production

Risk Assessment#

  • Risk level: low (1건 발생, 특정 이미지 크기 조건에서만 재현)
  • 예상 복잡도: trivial (extend 호출 전 음수 체크 및 resize 분기 추가)