mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Log zipUrl value in req-validation schema failure (#36603)
GitOrigin-RevId: c53b1d54e356eb937c12a3d06f620bd515af0042
This commit is contained in:
committed by
Copybot
parent
efd342919a
commit
fd0e0ac7df
@@ -32,6 +32,10 @@ const {
|
||||
* issues are logged). If it also fails, behavior falls back to logOnly
|
||||
* above, classifying/throwing from the fallback's ZodError when logOnly
|
||||
* is not set.
|
||||
* @property {string[]} [logFields] - Dotted field paths (e.g. 'body.zipUrl')
|
||||
* whose raw input values are resolved and included in the schema-failure
|
||||
* log entry. Values are only resolved on failure; strings are truncated
|
||||
* to 200 chars.
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -201,8 +205,10 @@ function sanitizeIssues(issues, depth = 0) {
|
||||
* @param {ZodType} schema - the primary schema; the dedup key.
|
||||
* @param {'log-only' | 'fallback-passed'} kind
|
||||
* @param {readonly ZodIssue[]} issues
|
||||
* @param {Request} input - the (possibly lockdown-unwrapped) request input
|
||||
* @param {string[]} [logFields] - dotted paths to resolve from input
|
||||
*/
|
||||
function logSchemaFailure(req, schema, kind, issues) {
|
||||
function logSchemaFailure(req, schema, kind, issues, input, logFields) {
|
||||
if (!logger) return
|
||||
|
||||
const sanitizedIssues = sanitizeIssues(issues)
|
||||
@@ -226,8 +232,32 @@ function logSchemaFailure(req, schema, kind, issues) {
|
||||
.slice(2, 7)
|
||||
.join('\n')
|
||||
|
||||
/** @type {Record<string, unknown> | undefined} */
|
||||
let resolvedFields
|
||||
if (logFields) {
|
||||
resolvedFields = {}
|
||||
for (const field of logFields) {
|
||||
let val = /** @type {any} */ (input)
|
||||
for (const seg of field.split('.')) {
|
||||
if (val == null || typeof val !== 'object') {
|
||||
val = '<missing>'
|
||||
break
|
||||
}
|
||||
val = val[seg]
|
||||
}
|
||||
resolvedFields[field] = typeof val === 'string' ? val.slice(0, 200) : val
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
{ location, kind, issues: sanitizedIssues, caller, req },
|
||||
{
|
||||
location,
|
||||
kind,
|
||||
issues: sanitizedIssues,
|
||||
caller,
|
||||
req,
|
||||
...(resolvedFields && { failingValues: resolvedFields }),
|
||||
},
|
||||
'req-validation: request failed schema in log-only rollout'
|
||||
)
|
||||
}
|
||||
@@ -274,12 +304,26 @@ function parseReq(req, schema, opts) {
|
||||
if (opts.fallbackSchema) {
|
||||
const fallbackParsed = opts.fallbackSchema.safeParse(input)
|
||||
if (fallbackParsed.success) {
|
||||
logSchemaFailure(req, schema, 'fallback-passed', parsed.error.issues)
|
||||
logSchemaFailure(
|
||||
req,
|
||||
schema,
|
||||
'fallback-passed',
|
||||
parsed.error.issues,
|
||||
input,
|
||||
opts.logFields
|
||||
)
|
||||
// The fallback's output, not T's -- see the return-type doc comment.
|
||||
return /** @type {output<T>} */ (fallbackParsed.data)
|
||||
}
|
||||
if (opts.logOnly) {
|
||||
logSchemaFailure(req, schema, 'log-only', parsed.error.issues)
|
||||
logSchemaFailure(
|
||||
req,
|
||||
schema,
|
||||
'log-only',
|
||||
parsed.error.issues,
|
||||
input,
|
||||
opts.logFields
|
||||
)
|
||||
// The raw, un-coerced input, not T's -- see the return-type doc
|
||||
// comment.
|
||||
return /** @type {output<T>} */ (input)
|
||||
@@ -289,7 +333,14 @@ function parseReq(req, schema, opts) {
|
||||
// preserve production's pre-refinement error shape.
|
||||
throwClassified(fallbackParsed.error)
|
||||
} else if (opts.logOnly) {
|
||||
logSchemaFailure(req, schema, 'log-only', parsed.error.issues)
|
||||
logSchemaFailure(
|
||||
req,
|
||||
schema,
|
||||
'log-only',
|
||||
parsed.error.issues,
|
||||
input,
|
||||
opts.logFields
|
||||
)
|
||||
// The raw, un-coerced input, not T's -- see the return-type doc
|
||||
// comment.
|
||||
return /** @type {output<T>} */ (input)
|
||||
|
||||
@@ -437,6 +437,105 @@ describe('parseReq log-only rollout', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('logFields', () => {
|
||||
it('resolves a dotted path and includes it in failingValues (logOnly)', () => {
|
||||
const req = { body: { zipUrl: '/bad/path' } } as Request
|
||||
const schema = z.object({
|
||||
body: z.object({ zipUrl: z.string().url() }),
|
||||
})
|
||||
|
||||
parseReq(req, schema, {
|
||||
logOnly: true,
|
||||
logFields: ['body.zipUrl'],
|
||||
})
|
||||
|
||||
expect(warnMock).toHaveBeenCalledTimes(1)
|
||||
const [ctx] = warnMock.mock.calls[0] as WarnCall
|
||||
expect(ctx.failingValues).toEqual({ 'body.zipUrl': '/bad/path' })
|
||||
})
|
||||
|
||||
it('resolves a dotted path and includes it in failingValues (fallback-passed)', () => {
|
||||
const req = { body: { zipUrl: '/bad/path' } } as Request
|
||||
const primary = z.object({
|
||||
body: z.object({ zipUrl: z.string().url() }),
|
||||
})
|
||||
const fallback = z.object({
|
||||
body: z.object({ zipUrl: z.string() }),
|
||||
})
|
||||
|
||||
parseReq(req, primary, {
|
||||
fallbackSchema: fallback,
|
||||
logFields: ['body.zipUrl'],
|
||||
})
|
||||
|
||||
expect(warnMock).toHaveBeenCalledTimes(1)
|
||||
const [ctx] = warnMock.mock.calls[0] as WarnCall
|
||||
expect(ctx.kind).toBe('fallback-passed')
|
||||
expect(ctx.failingValues).toEqual({ 'body.zipUrl': '/bad/path' })
|
||||
})
|
||||
|
||||
it('uses <missing> for a path that does not exist in the input', () => {
|
||||
const req = { body: {} } as Request
|
||||
const schema = z.object({
|
||||
body: z.object({ zipUrl: z.string() }),
|
||||
})
|
||||
|
||||
parseReq(req, schema, {
|
||||
logOnly: true,
|
||||
logFields: ['body.zipUrl'],
|
||||
})
|
||||
|
||||
expect(warnMock).toHaveBeenCalledTimes(1)
|
||||
const [ctx] = warnMock.mock.calls[0] as WarnCall
|
||||
expect(ctx.failingValues).toEqual({ 'body.zipUrl': undefined })
|
||||
})
|
||||
|
||||
it('uses <missing> when an intermediate segment is missing', () => {
|
||||
const req = { body: undefined } as unknown as Request
|
||||
const schema = z.object({
|
||||
body: z.object({ zipUrl: z.string() }),
|
||||
})
|
||||
|
||||
parseReq(req, schema, {
|
||||
logOnly: true,
|
||||
logFields: ['body.zipUrl'],
|
||||
})
|
||||
|
||||
expect(warnMock).toHaveBeenCalledTimes(1)
|
||||
const [ctx] = warnMock.mock.calls[0] as WarnCall
|
||||
expect(ctx.failingValues).toEqual({ 'body.zipUrl': '<missing>' })
|
||||
})
|
||||
|
||||
it('truncates string values longer than 200 chars', () => {
|
||||
const longValue = 'z'.repeat(250)
|
||||
const req = { body: { zipUrl: longValue } } as Request
|
||||
const schema = z.object({
|
||||
body: z.object({ zipUrl: z.string().url() }),
|
||||
})
|
||||
|
||||
parseReq(req, schema, {
|
||||
logOnly: true,
|
||||
logFields: ['body.zipUrl'],
|
||||
})
|
||||
|
||||
expect(warnMock).toHaveBeenCalledTimes(1)
|
||||
const [ctx] = warnMock.mock.calls[0] as WarnCall
|
||||
expect(ctx.failingValues['body.zipUrl']).toHaveLength(200)
|
||||
expect(ctx.failingValues['body.zipUrl']).toBe(longValue.slice(0, 200))
|
||||
})
|
||||
|
||||
it('omits failingValues when logFields is not set', () => {
|
||||
const req = { body: { name: 1234 } } as Request
|
||||
const schema = z.object({ body: z.object({ name: z.string() }) })
|
||||
|
||||
parseReq(req, schema, { logOnly: true })
|
||||
|
||||
expect(warnMock).toHaveBeenCalledTimes(1)
|
||||
const [ctx] = warnMock.mock.calls[0] as WarnCall
|
||||
expect(ctx.failingValues).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('success path', () => {
|
||||
it('enforce mode: returns the parsed data and logs nothing, even with opts set', () => {
|
||||
setReqValidationModeForTests('enforce')
|
||||
|
||||
Reference in New Issue
Block a user