feat(api): permitir If-Match en PATCH del proxy para concurrencia optimista
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d49b87a2ce
commit
c66ad96e4a
3 files changed
+19
-2
No files matched your search
@@ -71,6 +71,13 @@ describe('entityPath', () => {
|
||||
});
|
||||
|
||||
describe('pickSlHeaders', () => {
|
||||
it('deja pasar If-Match solo al modificar', () => {
|
||||
expect(pickSlHeaders({ 'if-match': ' W/"1A2B" ' }, 'PATCH')).toEqual({ 'If-Match': 'W/"1A2B"' });
|
||||
expect(() => pickSlHeaders({ 'if-match': 'W/"1"' }, 'POST')).toThrow(BadRequestException);
|
||||
expect(() => pickSlHeaders({ 'if-match': 'a\r\nb' }, 'PATCH')).toThrow(BadRequestException);
|
||||
expect(() => pickSlHeaders({ 'if-match': '' }, 'PATCH')).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('deja pasar solo las B1S-* permitidas para el método', () => {
|
||||
expect(pickSlHeaders({ 'b1s-replacecollectionsonpatch': 'true', host: 'x', cookie: 'y' }, 'PATCH')).toEqual({
|
||||
'B1S-ReplaceCollectionsOnPatch': 'true',
|
||||
|
||||
@@ -49,7 +49,7 @@ export type GetQuery = z.infer<typeof getQuerySchema>;
|
||||
|
||||
export const bodySchema = z.record(z.string(), z.unknown());
|
||||
|
||||
/** Cabeceras B1S-* que el frontend puede decidir; cualquier otra B1S-* se rechaza. */
|
||||
/** Cabeceras B1S-* que el frontend puede decidir (más `If-Match` al modificar); cualquier otra B1S-* se rechaza. */
|
||||
const B1S_HEADERS: Record<string, { name: string; methods: string[]; valid: (v: string) => boolean }> = {
|
||||
'b1s-replacecollectionsonpatch': {
|
||||
name: 'B1S-ReplaceCollectionsOnPatch',
|
||||
@@ -64,11 +64,21 @@ const B1S_HEADERS: Record<string, { name: string; methods: string[]; valid: (v:
|
||||
},
|
||||
};
|
||||
|
||||
/** Etag de concurrencia optimista (`odata.etag` del documento leído, ej. `W/"1A2B"`): solo al modificar. */
|
||||
const isEtag = (v: string) => v.length >= 1 && v.length <= 200 && noControlChars(v);
|
||||
|
||||
export function pickSlHeaders(
|
||||
incoming: Record<string, string | string[] | undefined>,
|
||||
method: string,
|
||||
): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
const ifMatch = incoming['if-match'];
|
||||
if (ifMatch !== undefined) {
|
||||
if (method !== 'PATCH' || typeof ifMatch !== 'string' || !isEtag(ifMatch.trim())) {
|
||||
throw new BadRequestException('Cabecera no permitida o inválida: if-match');
|
||||
}
|
||||
out['If-Match'] = ifMatch.trim();
|
||||
}
|
||||
for (const [rawName, value] of Object.entries(incoming)) {
|
||||
if (!rawName.startsWith('b1s-')) continue;
|
||||
const rule = B1S_HEADERS[rawName];
|
||||
|
||||
Reference in new issue
Block a user