fix(agiloft): resolve attachment MIME type instead of trusting the header (#6573)

Agiloft labels most attachments application/octet-stream whatever they
actually are, so a downstream consumer keyed on the header mis-handles
them. Resolve the type through resolveEffectiveMimeType, the shared helper
the other tool routes already use, which prefers a header that names a
real format and otherwise falls back to the filename Agiloft sends in
Content-Disposition.
This commit is contained in:
Waleed
2026-08-11 17:09:11 -07:00
committed by GitHub
parent 9b9f4ee596
commit 05de46357a
2 changed files with 39 additions and 2 deletions
@@ -140,6 +140,35 @@ describe('POST /api/tools/agiloft/retrieve', () => {
expect(inputValidationMockFns.mockValidateUrlWithDNS).toHaveBeenCalledTimes(1)
})
it('resolves the real type when Agiloft labels an attachment octet-stream', async () => {
const fileBytes = Buffer.from('PKdocx-bytes', 'utf-8')
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce(
mockSecureFetchResponse({
arrayBuffer: fileBytes.buffer.slice(
fileBytes.byteOffset,
fileBytes.byteOffset + fileBytes.byteLength
) as ArrayBuffer,
headers: new Headers({
'content-type': 'application/octet-stream',
'content-disposition': 'attachment; filename="Master Agreement.docx"',
}),
})
)
const response = await POST(createMockRequest('POST', baseBody))
const data = (await response.json()) as { output: { file: { mimeType: string } } }
/**
* Agiloft labels most attachments octet-stream whatever they are. The
* filename disambiguates what the leading bytes cannot: a ZIP header is
* equally a .docx, .xlsx or a plain archive.
*/
expect(data.output.file.mimeType).toBe(
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
})
it('propagates upstream errors', async () => {
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce(
mockSecureFetchResponse({ ok: false, status: 404, text: 'Record not found' })
@@ -7,6 +7,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { resolveEffectiveMimeType } from '@/lib/uploads/utils/file-utils'
import { isEwRestBody } from '@/tools/agiloft/ewrest'
import {
AGILOFT_MAX_ATTACHMENT_BYTES,
@@ -128,10 +129,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
/**
* Agiloft labels most attachments application/octet-stream whatever they
* are, so downstream consumers keyed on the header mis-handle them. The
* filename Agiloft sends in Content-Disposition carries the real type.
*/
const mimeType = resolveEffectiveMimeType(contentType, fileName)
logger.info(`[${requestId}] Attachment downloaded successfully`, {
name: fileName,
size: fileBuffer.length,
mimeType: contentType,
mimeType,
})
const base64Data = fileBuffer.toString('base64')
@@ -141,7 +149,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
output: {
file: {
name: fileName,
mimeType: contentType,
mimeType,
data: base64Data,
size: fileBuffer.length,
},