fix: handle double quotes in etag (#8362)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2025-03-27 13:05:25 +07:00 committed by GitHub
parent 3adc00dced
commit 57cd51ee1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 118 additions and 5 deletions

View File

@ -0,0 +1,40 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { wrapETag, unwrapETag } from '../utils'
describe('unwrapETag', () => {
it('should unwrap weak validator prefix', () => {
expect(unwrapETag('W/"abc"')).toBe('abc')
})
it('should unwrap strong validator prefix', () => {
expect(unwrapETag('"abc"')).toBe('abc')
})
it('should unwrap no validator prefix', () => {
expect(unwrapETag('abc')).toBe('abc')
})
})
describe('wrapETag', () => {
it('should wrap strong validator prefix', () => {
expect(wrapETag('abc')).toBe('"abc"')
})
it('should wrap weak validator prefix', () => {
expect(wrapETag('abc', true)).toBe('W/"abc"')
})
})

View File

@ -19,6 +19,7 @@ import fetch, { type RequestInfo, type RequestInit, type Response } from 'node-f
import { Readable } from 'stream'
import { DatalakeError, NetworkError, NotFoundError } from './error'
import { unwrapETag } from './utils'
/** @public */
export interface ObjectMetadata {
@ -191,7 +192,7 @@ export class DatalakeClient {
lastModified: isNaN(lastModified) ? 0 : lastModified,
size: isNaN(size) ? 0 : size,
type: headers.get('Content-Type') ?? '',
etag: headers.get('ETag') ?? ''
etag: unwrapETag(headers.get('ETag') ?? '')
}
}

View File

@ -0,0 +1,32 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
export function unwrapETag (etag: string): string {
if (etag.startsWith('W/')) {
etag = etag.substring(2)
}
if (etag.startsWith('"') && etag.endsWith('"')) {
etag = etag.slice(1, -1)
}
return etag
}
export function wrapETag (etag: string, weak: boolean = false): string {
etag = unwrapETag(etag)
const quoted = etag.startsWith('"') ? etag : `"${etag}"`
return weak ? `W/${quoted}` : quoted
}

View File

@ -15,3 +15,4 @@
export * from './datalake'
export * from './types'
export * from './utils'

View File

@ -0,0 +1,39 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
export function unwrapETag (etag: string): string {
// Remove weak validator prefix 'W/'
if (etag.startsWith('W/')) {
etag = etag.substring(2)
}
// Remove surrounding quotes
if (etag.startsWith('"') && etag.endsWith('"')) {
etag = etag.slice(1, -1)
}
return etag
}
export function wrapETag (etag: string, weak: boolean = false): string {
// Remove any existing wrapping first to ensure clean wrap
etag = unwrapETag(etag)
// Add quotes if not present
const quoted = etag.startsWith('"') ? etag : `"${etag}"`
// Add weak prefix if requested
return weak ? `W/${quoted}` : quoted
}

View File

@ -19,7 +19,7 @@ import { UploadedFile } from 'express-fileupload'
import fs from 'fs'
import { cacheControl } from '../const'
import { type Datalake } from '../datalake'
import { type Datalake, wrapETag } from '../datalake'
import { getBufferSha256, getStreamSha256 } from '../hash'
interface BlobParentRequest {
@ -70,11 +70,11 @@ export async function handleBlobGet (
)
res.setHeader('Cache-Control', blob.cacheControl ?? cacheControl)
res.setHeader('Last-Modified', new Date(blob.lastModified).toUTCString())
res.setHeader('ETag', blob.etag)
res.setHeader('ETag', wrapETag(blob.etag))
if (range != null && blob.bodyRange !== undefined) {
res.setHeader('Content-Range', blob.bodyRange)
res.setHeader('ETag', blob.bodyLength !== blob.size ? blob.bodyEtag : blob.etag)
res.setHeader('ETag', wrapETag(blob.bodyLength !== blob.size ? blob.bodyEtag : blob.etag))
}
const status = range != null && blob.bodyLength !== blob.size ? 206 : 200
@ -120,7 +120,7 @@ export async function handleBlobHead (
res.setHeader('Content-Disposition', filename !== undefined ? `attachment; filename="${filename}"` : 'attachment')
res.setHeader('Cache-Control', head.cacheControl ?? cacheControl)
res.setHeader('Last-Modified', new Date(head.lastModified).toUTCString())
res.setHeader('ETag', head.etag)
res.setHeader('ETag', wrapETag(head.etag))
res.status(200).send()
}