Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4 programming model - req.json is returning Body is unusable for post with JSON #79

Closed
diberry opened this issue Apr 10, 2023 · 7 comments

Comments

@diberry
Copy link

diberry commented Apr 10, 2023

Local development with new Node.js programming model.

I'm sending in JSON in a curl command:

curl --location 'http://localhost:7071/api/worldFromText' --data '{"id":"abc"}' --header 'Content-Type: application/json' --verbose

I've also tried it with a data file from cURL as well as PostMan. I must be missing something. I have 2 functions, 1 uses req.text and parses the JSON which works. 1 reads the req.json and throws an error. Where am I going wrong with this? The undici-fetch error makes it look like I can't do a POST in the new runtime with JSON because their fix is to downgrade to Node 16.

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";

const worlds: string[] = [];

function generateRandomNumber() {
    return Math.floor((Math.random() * 100) + 1);
}

function processError(err: unknown): any {
    if (typeof err === 'string') {
        return { body: err?.toUpperCase(), status: 500 };
    } else if (
        err['stack'] &&
        process.env?.NODE_ENV?.toLowerCase() !== 'production'
    ) {
        return { jsonBody: { stack: err['stack'], message: err['message'] } };
    } else if (err instanceof Error) {
        return { body: err?.message, status: 500 };
    } else {
        return { body: JSON.stringify(err) };
    }
}
// curl --location 'http://localhost:7071/api/worldFromText' --data '{"id":"abc"}' --header 'Content-Type: application/json' --verbose
app.post("addWorldText", {
    route: "worldFromText",
    handler: async (req, context) => {
        try {

            const bodyAsText = await req.text();
            const id:string = JSON.parse(bodyAsText)?.id;

            worlds.push(id);
            return {
                jsonBody: { id }
            }
        } catch (err: unknown) {
            return processError(err);
        }

    }
})
// curl --location 'http://localhost:7071/api/worldFromJson' --data '{"id":"abc"}' --header 'Content-Type: application/json' --verbose
// curl --location 'http://localhost:7071/api/worldFromJson' --data @mydata.json --header 'Content-Type: application/json' --verbose
type WorldJsonBody = {
    id: string;
}
app.post("addWorldJson", {
    route: "worldFromJson",
    handler: async (req, context) => {
        try {

            // debug only to see what came in
            const allText = await req.text();
            context.log(allText)

            const body: WorldJsonBody = await req.json() as WorldJsonBody;

            worlds.push(body?.id);
            return {
                jsonBody: { id: body?.id }
            }
        } catch (err: unknown) {
            return processError(err);
        }

    }
})
// curl --location 'http://localhost:7071/api/worlds2' 
app.get("getWorlds2", {
    route: "worlds2",
    handler: (req, context) => {
        try {
            return {
                jsonBody: {
                    worlds
                }
            }
        } catch (err: unknown) {
            return processError(err);
        }
    }
})

Complete error:

node ➜ /workspaces/20230410 $ curl --location 'http://localhost:7071/api/worldFromJson' --data @mydata.json --header 'Content-Type: application/json' --verbose
*   Trying 127.0.0.1:7071...
* Connected to localhost (127.0.0.1) port 7071 (#0)
> POST /api/worldFromJson HTTP/1.1
> Host: localhost:7071
> User-Agent: curl/7.74.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 12
> 
* upload completely sent off: 12 out of 12 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Mon, 10 Apr 2023 21:21:06 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
{"stack":"TypeError: Body is unusable\n    at specConsumeBody (/workspaces/20230410/node_modules/undici/lib/fetch/body.js:497:11)\n    at Request.json (/workspaces/20230410/node_modules/undici/lib/fetch/body.js:360:14)\n    at HttpRequest.<anonymous> (/workspaces/20230410/node_modules/@azure/functions/dist/azure-functions.js:1236:73)\n    at Generator.next (<anonymous>)\n    at /workspaces/20230410/node_modules/@azure/functions/dist/azure-functions.js:1155:71\n    at new Promise (<anonymous>)\n    at __webpack_modules__../src/http/HttpRequest.ts.__awaiter (/workspaces/20230410/node_modules/@azure/functions/dist/azure-functions.js:1151:12)\n    at HttpRequest.json (/workspaces/20230410/node_modules/@azure/functions/dist/azure-functions.js:1235:16)\n    at /workspaces/20230410/dist/src/functions/helloworld2.js:58:36\n    at Generator.next (<anonymous>)","message":"Body is unusable"}node ➜ /workspaces/20230410 $ 
@ejizba
Copy link
Contributor

ejizba commented Apr 10, 2023

Try removing these lines:

// debug only to see what came in
const allText = await req.text();
context.log(allText)

The request body functions can be run only once; subsequent calls will resolve with empty strings/ArrayBuffers.

@diberry
Copy link
Author

diberry commented Apr 10, 2023

That works for me. Thanks.

@diberry diberry closed this as completed Apr 10, 2023
@arlogilbert
Copy link

What is the rationale behind making the body only available for a single request?

There are plenty of times where we need to access the raw body to do things such as validate signatures from third parties for security purposes, those generally need the rawbody (v3).

Then separately we want to do crazy things like pull keys out of the JSON that was sent to us. Currently our solution is to do a JSON.parse() on the text output, but somehow hash of the raw body is different than the hash of the parsed JSON, even though it is well formed.

Love the v4 model, but wow there are some big differences that are making life harder.

@ejizba
Copy link
Contributor

ejizba commented Dec 15, 2023

Hi @arlogilbert, good questions. First of all, we're leveraging a design by the much larger Node.js community called the fetch standard (some docs here). In the v4 model we wanted to leverage an existing design for http types instead of using one unique to Azure Functions. Other folks have asked your same question to the larger community, and here is one example.

AFAIK, the primary reason is to allow the use of streams under the covers and by definition streams can only be read once. Using a stream has big performance and throughput benefits, so we've been preparing for that for a while. If you want to get into the nitty gritty, the functions implementation doesn't technically use a stream yet, but it will very soon and moving to these community-designed types was a first step in the process.

I believe you're already following the recommended workaround, which is to call .text(), save that value, and then call JSON.parse on it when needed. It's not clear to me why that doesn't work for you, but if you elaborate with sample data and sample code I might be able to help.

@arlogilbert
Copy link

arlogilbert commented Dec 16, 2023 via email

@restfulhead
Copy link

restfulhead commented Jan 3, 2024

@ejizba I understand the design choice and looking forward to stream support. However, it would be beneficial to have an option to cache the request body. I'm writing a hook that validates the request body. The hook consumes it using .json(). Now the function has no way to access the request body. Because everything is readonly in HttpRequest I also don't see an easy option to reset/replace the consumed stream. Can you provide guidance how a preInvocation hook and a function can use the request body?

Update: I've tried to use pipeThrough, but that still causes the bodyUsed flag to be true. Looks like the only option is to copy the entire request:

  const textBody = await request.text()
  parsedBody = JSON.parse(textBody)

  // TODO: validate body here

  const headerCopy: Record<string, string> = {}
  origRequest.headers?.forEach((value, key) => {
    headerCopy[key] = value
  })

  const queryParams: Record<string, string> = {}
  origRequest.query?.forEach((value, key) => {
    queryParams[key] = value
  })

  request = new HttpRequest({
    method: origRequest.method,
    url: origRequest.url,
    body: { string: textBody },
    headers: headerCopy,
    query: queryParams,
    params: origRequest.params,
  })

@ejizba
Copy link
Contributor

ejizba commented Jan 3, 2024

@restfulhead moved your comment to a new issue: #207

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants