hono icon indicating copy to clipboard operation
hono copied to clipboard

Inconsistent Request Body Caching Between c.req.json() and Validator Middleware

Open zsh-eng opened this issue 1 year ago • 1 comments

What version of Hono are you using?

3.12.11

What runtime/platform is your app running on?

Cloudflare Workers

What steps can reproduce the bug?

I've encountered an issue with how Hono caches the request body in different formats, leading to unexpected behavior when using middlewares in a specific order.

When one middleware calls c.req.json(), it updates bodyCache[json] without also updating bodyCache[arrayBuffer]. If we later use the validator middleware, which expects the arrayBuffer to be cached, it results in an error for a malformed request.

app.post(
  '/test',
  async (c, next) => {
    const body = await c.req.json();
    await next();
  },
  validator('json', (value, c) => {
    return value;
  }),
  async (c) => {
    return c.json({ message: 'OK' }, 200);
  },
);

What is the expected behavior?

The request passes through both middlewares without issues, with the validator correctly validating the parsed JSON.

What do you see instead?

Receive a 400 Bad Request error.

[  <-- POST /TEST]: undefined
✘ [ERROR] Error: Malformed JSON in request body

Additional information

The validator middleware throws an error indicating a malformed request body, despite the body being valid JSON. This seems to occur because c.req.json() does not update bodyCache[arrayBuffer], leading to a mismatch in the cached body data.

// request.ts
  private cachedBody = (key: keyof Body) => {
// ...
    if (bodyCache.arrayBuffer) {
      return (async () => {
        return await new Response(bodyCache.arrayBuffer)[key]()
      })()
    }
    return (bodyCache[key] = raw[key]()) // doesn't update arrayBuffer
  }
// validator/validator.ts
        /**
         * Get the arrayBuffer first, create JSON object via Response,
         * and cache the arrayBuffer in the c.req.bodyCache.
         */
        try {
          const arrayBuffer = c.req.bodyCache.arrayBuffer ?? (await c.req.raw.arrayBuffer()) // Uses arrayBuffer
          value = await new Response(arrayBuffer).json()
          c.req.bodyCache.json = value
          c.req.bodyCache.arrayBuffer = arrayBuffer
        } catch {
          console.error('Error: Malformed JSON in request body')
          return c.json(
            {
              success: false,
              message: 'Malformed JSON in request body',
            },
            400
          )
        }

zsh-eng avatar Feb 07 '24 07:02 zsh-eng

@zsh-eng

Thanks. This should be fixed. I'll work on it later.

yusukebe avatar Feb 07 '24 17:02 yusukebe

It looks like this issue is still present when reading the request body as .text()

sam-potter avatar Mar 19 '24 08:03 sam-potter

@sam-potter

You are right. Bugs still remain.

yusukebe avatar Mar 20 '24 12:03 yusukebe

There's no way you fixed it that fast, you're a beast 🙏 @yusukebe

sam-potter avatar Mar 20 '24 23:03 sam-potter

@sam-potter @yusukebe Issue is still there as it treats object as promise

Custom Middlware:

app.use('*', async (c: Context, next) => {
  if (c.req.method == 'POST') {
    const authHeader = c.req.header('PayloadToken');
    if (!authHeader) {
      return c.json('Error in header');
    }
    const secret = c.env.HMAC_SECRET;
    const payload = await c.req.json();
    const calculatedHmac = CryptoJS.HmacSHA256(payload, secret);
    const calculatedHmacString = calculatedHmac.toString(CryptoJS.enc.Hex);
    if (calculatedHmacString != authHeader) {
      return c.json('Unauthorized: Invalid HMAC token', { status: 401 });
    }
  }
  await next();
});

route:

  app.post(
    'enrollmentSettings',
    zValidator('json', joiningRoleSchema),
    setMerchantConfig,
  );

z schema:

  export const joiningRoleSchema = z.object({
  role: z.string(),
  storeId: z.string(),
});

Error:

  {
    "success": false,
    "error": {
        "issues": [
            {
                "code": "invalid_type",
                "expected": "object",
                "received": "promise",
                "path": [],
                "message": "Expected object, received promise"
            }
        ],
        "name": "ZodError"
    }
}

If I comment the line const payload = await c.req.json(); in the middleware, validation pass otherwise fails

osameyy avatar Mar 27 '24 20:03 osameyy

@osameyy Thanks!

I noticed the same thing yesterday! I'll fix it soon.

yusukebe avatar Mar 27 '24 20:03 yusukebe

Thanks @yusukebe Its Working now!

osameyy avatar Mar 28 '24 00:03 osameyy