Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure/functions",
"version": "4.16.2",
"version": "4.16.3",
"description": "Microsoft Azure Functions NodeJS Framework",
"keywords": [
"azure",
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

export const version = '4.16.2';
export const version = '4.16.3';

export const returnBindingKey = '$return';
25 changes: 20 additions & 5 deletions src/http/HttpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { fromNullableMapping } from '../converters/fromRpcNullable';
import { fromRpcTypedData } from '../converters/fromRpcTypedData';
import { AzFuncSystemError } from '../errors';
import { isDefined, nonNullProp } from '../utils/nonNull';
import { workerSystemLog } from '../utils/workerSystemLog';
import { extractHttpUserFromHeaders } from './extractHttpUserFromHeaders';

interface InternalHttpRequestInit extends RpcHttpData {
Expand All @@ -33,17 +34,31 @@ export class HttpRequest implements types.HttpRequest {
let nativeReq = init.nativeRequest;
if (!nativeReq) {
const url = nonNullProp(init, 'url');
const method = nonNullProp(init, 'method');

// GET/HEAD requests cannot have a body. The global Request constructor throws if one is
// provided, so omit it for these methods to match the streaming path (createStreamRequest).
// See https://github.com/Azure/azure-functions-nodejs-library/issues/458
let body: Buffer | string | undefined;
if (init.body?.bytes) {
body = Buffer.from(init.body?.bytes);
} else if (init.body?.string) {
body = init.body.string;
const lowerMethod = method.toLowerCase();
if (lowerMethod !== 'get' && lowerMethod !== 'head') {
Comment thread
swapnil-nagar marked this conversation as resolved.
if (init.body?.bytes) {
body = Buffer.from(init.body?.bytes);
} else if (init.body?.string) {
body = init.body.string;
}
} else if (init.body?.bytes?.length || init.body?.string) {
// The incoming GET/HEAD request has a body, which the global Request constructor forbids.
// We drop it to avoid crashing, but log it so the behavior isn't silent and is easier to debug.
workerSystemLog(
'warning',
`Discarding the body of the incoming ${method} request because GET and HEAD requests cannot have a body.`
);
}

nativeReq = new Request(url, {
body,
method: nonNullProp(init, 'method'),
method,
headers: fromNullableMapping(init.nullableHeaders, init.headers),
});
}
Expand Down
13 changes: 11 additions & 2 deletions src/http/HttpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ interface InternalHttpResponseInit extends HttpResponseInit {
nativeResponse?: Response;
}

// Statuses that cannot have a body per the WHATWG fetch spec ("null body status"), limited to the
// valid Response status range (200-599). The global Response constructor throws if a body is provided.
const nullBodyStatuses = new Set([204, 205, 304]);

export class HttpResponse implements types.HttpResponse {
readonly cookies: types.Cookie[];
readonly enableContentNegotiation: boolean;
Expand All @@ -26,7 +30,12 @@ export class HttpResponse implements types.HttpResponse {
this.#nativeRes = init.nativeResponse;
} else {
const resInit: ResponseInit = { status: init.status, headers: init.headers };
if (isDefined(init.jsonBody)) {
// 204/205/304 responses cannot have a body. The global Response constructor throws if one
// is provided, so omit it to avoid crashing when converting a handler's response. This
// mirrors the GET/HEAD body handling in HttpRequest.
// See https://github.com/Azure/azure-functions-nodejs-library/issues/458
const isNullBodyStatus = isDefined(init.status) && nullBodyStatuses.has(init.status);
if (isDefined(init.jsonBody) && !isNullBodyStatus) {
// Response.json is not available in all versions, so we create it manually
const jsonBody = JSON.stringify(init.jsonBody);
const jsonHeaders = new Headers(resInit.headers);
Expand All @@ -38,7 +47,7 @@ export class HttpResponse implements types.HttpResponse {
// Cast to any to satisfy the native Response constructor
// Our HttpResponseBodyInit type is compatible with what Node.js accepts
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.#nativeRes = new Response(init.body as any, resInit);
this.#nativeRes = new Response(isNullBodyStatus ? undefined : (init.body as any), resInit);
}
}

Expand Down
17 changes: 17 additions & 0 deletions test/converters/toRpcHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,23 @@ describe('toRpcHttp', () => {
await expect(toRpcHttp('invocId', { status: {} })).to.eventually.be.rejectedWith(/status/i);
});

// https://github.com/Azure/azure-functions-nodejs-library/issues/458 (Response-side parallel):
// null-body statuses must not crash conversion when a handler returns a body.
Comment thread
swapnil-nagar marked this conversation as resolved.
it('status 204 with a body converts to an empty body', async () => {
const result = await toRpcHttp('invocId', { status: 204, body: 'dropped' });
expect(result).to.deep.equal(getExpectedRpcHttp('', {}, '204'));
});

it('status 205 with a body converts to an empty body', async () => {
const result = await toRpcHttp('invocId', { status: 205, body: 'dropped' });
expect(result).to.deep.equal(getExpectedRpcHttp('', {}, '205'));
});

it('status 304 with a jsonBody converts to an empty body', async () => {
const result = await toRpcHttp('invocId', { status: 304, jsonBody: { a: 1 } });
expect(result).to.deep.equal(getExpectedRpcHttp('', {}, '304'));
});

it('headers object', async () => {
const result = await toRpcHttp('invocId', {
headers: {
Expand Down
Loading
Loading