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
28 changes: 3 additions & 25 deletions package-lock.json

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

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure/functions",
"version": "4.10.0",
"version": "4.11.0",
"description": "Microsoft Azure Functions NodeJS Framework",
"keywords": [
"azure",
Expand Down Expand Up @@ -43,8 +43,7 @@
"dependencies": {
"@azure/functions-extensions-base": "0.2.0",
"cookie": "^0.7.0",
"long": "^4.0.0",
"undici": "^5.29.0"
"long": "^4.0.0"
},
"devDependencies": {
"@types/chai": "^4.2.22",
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.10.0';
export const version = '4.11.0';

export const returnBindingKey = '$return';
45 changes: 23 additions & 22 deletions src/http/HttpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Blob } from 'buffer';
import { IncomingMessage } from 'http';
import * as stream from 'stream';
import { ReadableStream } from 'stream/web';
import { FormData, Headers, HeadersInit, Request as uRequest } from 'undici';
import { URLSearchParams } from 'url';
import { fromNullableMapping } from '../converters/fromRpcNullable';
import { fromRpcTypedData } from '../converters/fromRpcTypedData';
Expand All @@ -17,22 +16,22 @@ import { isDefined, nonNullProp } from '../utils/nonNull';
import { extractHttpUserFromHeaders } from './extractHttpUserFromHeaders';

interface InternalHttpRequestInit extends RpcHttpData {
undiciRequest?: uRequest;
nativeRequest?: Request;
}

export class HttpRequest implements types.HttpRequest {
readonly query: URLSearchParams;
readonly params: HttpRequestParams;

#cachedUser?: HttpRequestUser | null;
#uReq: uRequest;
#nativeReq: Request;
#init: InternalHttpRequestInit;

constructor(init: InternalHttpRequestInit) {
this.#init = init;

let uReq = init.undiciRequest;
if (!uReq) {
let nativeReq = init.nativeRequest;
if (!nativeReq) {
const url = nonNullProp(init, 'url');

let body: Buffer | string | undefined;
Expand All @@ -42,33 +41,33 @@ export class HttpRequest implements types.HttpRequest {
body = init.body.string;
}

uReq = new uRequest(url, {
nativeReq = new Request(url, {
body,
method: nonNullProp(init, 'method'),
headers: fromNullableMapping(init.nullableHeaders, init.headers),
});
}
this.#uReq = uReq;
this.#nativeReq = nativeReq;

if (init.nullableQuery || init.query) {
this.query = new URLSearchParams(fromNullableMapping(init.nullableQuery, init.query));
} else {
this.query = new URL(this.#uReq.url).searchParams;
this.query = new URL(this.#nativeReq.url).searchParams;
}

this.params = fromNullableMapping(init.nullableParams, init.params);
}

get url(): string {
return this.#uReq.url;
return this.#nativeReq.url;
}

get method(): string {
return this.#uReq.method;
return this.#nativeReq.method;
}

get headers(): Headers {
return this.#uReq.headers;
return this.#nativeReq.headers;
}

get user(): HttpRequestUser | null {
Expand All @@ -80,36 +79,36 @@ export class HttpRequest implements types.HttpRequest {
}

get body(): ReadableStream<any> | null {
return this.#uReq.body;
return this.#nativeReq.body as ReadableStream<any> | null;
}

get bodyUsed(): boolean {
return this.#uReq.bodyUsed;
return this.#nativeReq.bodyUsed;
}

async arrayBuffer(): Promise<ArrayBuffer> {
return this.#uReq.arrayBuffer();
return this.#nativeReq.arrayBuffer();
}

async blob(): Promise<Blob> {
return this.#uReq.blob();
return this.#nativeReq.blob() as Promise<Blob>;
}

async formData(): Promise<FormData> {
return this.#uReq.formData();
return this.#nativeReq.formData();
}

async json(): Promise<unknown> {
return this.#uReq.json();
return this.#nativeReq.json();
}

async text(): Promise<string> {
return this.#uReq.text();
return this.#nativeReq.text();
}

clone(): HttpRequest {
const newInit = structuredClone(this.#init);
newInit.undiciRequest = this.#uReq.clone();
newInit.nativeRequest = this.#nativeReq.clone();
return new HttpRequest(newInit);
}
}
Expand Down Expand Up @@ -144,8 +143,10 @@ export function createStreamRequest(
headers = <HeadersInit>headersData;
}

const uReq = new uRequest(url, {
body,
const nativeReq = new Request(url, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
body: body as any,
// @ts-expect-error duplex is needed for streaming but not in all TypeScript versions
duplex: 'half',
method: nonNullProp(proxyReq, 'method'),
headers,
Expand All @@ -159,7 +160,7 @@ export function createStreamRequest(
}

return new HttpRequest({
undiciRequest: uReq,
nativeRequest: nativeReq,
params,
});
}
41 changes: 23 additions & 18 deletions src/http/HttpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,37 @@ import * as types from '@azure/functions';
import { HttpResponseInit } from '@azure/functions';
import { Blob } from 'buffer';
import { ReadableStream } from 'stream/web';
import { FormData, Headers, Response as uResponse, ResponseInit as uResponseInit } from 'undici';
import { isDefined } from '../utils/nonNull';

interface InternalHttpResponseInit extends HttpResponseInit {
undiciResponse?: uResponse;
nativeResponse?: Response;
}

export class HttpResponse implements types.HttpResponse {
readonly cookies: types.Cookie[];
readonly enableContentNegotiation: boolean;

#uRes: uResponse;
#nativeRes: Response;
#init: InternalHttpResponseInit;

constructor(init?: InternalHttpResponseInit) {
init ??= {};
this.#init = init;

if (init.undiciResponse) {
this.#uRes = init.undiciResponse;
if (init.nativeResponse) {
this.#nativeRes = init.nativeResponse;
} else {
const uResInit: uResponseInit = { status: init.status, headers: init.headers };
const resInit: ResponseInit = { status: init.status, headers: init.headers };
if (isDefined(init.jsonBody)) {
this.#uRes = uResponse.json(init.jsonBody, uResInit);
// 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);
if (!jsonHeaders.has('content-type')) {
jsonHeaders.set('content-type', 'application/json');
}
this.#nativeRes = new Response(jsonBody, { ...resInit, headers: jsonHeaders });
} else {
this.#uRes = new uResponse(init.body, uResInit);
this.#nativeRes = new Response(init.body, resInit);
}
}

Expand All @@ -39,44 +44,44 @@ export class HttpResponse implements types.HttpResponse {
}

get status(): number {
return this.#uRes.status;
return this.#nativeRes.status;
}

get headers(): Headers {
return this.#uRes.headers;
return this.#nativeRes.headers;
}

get body(): ReadableStream<any> | null {
return this.#uRes.body;
return this.#nativeRes.body as ReadableStream<any> | null;
}

get bodyUsed(): boolean {
return this.#uRes.bodyUsed;
return this.#nativeRes.bodyUsed;
}

async arrayBuffer(): Promise<ArrayBuffer> {
return this.#uRes.arrayBuffer();
return this.#nativeRes.arrayBuffer();
}

async blob(): Promise<Blob> {
return this.#uRes.blob();
return this.#nativeRes.blob() as Promise<Blob>;
}

async formData(): Promise<FormData> {
return this.#uRes.formData();
return this.#nativeRes.formData();
}

async json(): Promise<unknown> {
return this.#uRes.json();
return this.#nativeRes.json();
}

async text(): Promise<string> {
return this.#uRes.text();
return this.#nativeRes.text();
}

clone(): HttpResponse {
const newInit = structuredClone(this.#init);
newInit.undiciResponse = this.#uRes.clone();
newInit.nativeResponse = this.#nativeRes.clone();
return new HttpResponse(newInit);
}
}
1 change: 0 additions & 1 deletion src/http/extractHttpUserFromHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT License.

import { HttpRequestUser } from '@azure/functions';
import { Headers } from 'undici';
import { nonNullValue } from '../utils/nonNull';

/* grandfathered in. Should fix when possible */
Expand Down
1 change: 0 additions & 1 deletion test/converters/toRpcHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'mocha';
import * as chai from 'chai';
import { expect } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { Headers } from 'undici';
import { toRpcHttp } from '../../src/converters/toRpcHttp';
import { HttpResponse } from '../../src/http/HttpResponse';

Expand Down
8 changes: 5 additions & 3 deletions test/http/HttpRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'mocha';
import * as chai from 'chai';
import { expect } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { File } from 'undici';
import { HttpRequest } from '../../src/http/HttpRequest';

chai.use(chaiAsPromised);
Expand Down Expand Up @@ -94,7 +93,7 @@ world

const parsedForm = await req.formData();
expect(parsedForm.has('myfile')).to.equal(true);
const file = <File>parsedForm.get('myfile');
const file = parsedForm.get('myfile') as File;
expect(file.name).to.equal('test.txt');
expect(file.type).to.equal('text/plain');
expect(await file.text()).to.equal(`hello\r\nworld`);
Expand Down Expand Up @@ -135,7 +134,10 @@ value2
const contentTypes = ['application/octet-stream', 'application/json', 'text/plain', 'invalid'];
for (const contentType of contentTypes) {
const req = createFormRequest('', contentType);
await expect(req.formData()).to.eventually.be.rejectedWith(/Could not parse content as FormData/i);
// Native fetch API has different error message than undici
await expect(req.formData()).to.eventually.be.rejectedWith(
/Content-Type.*not.*one of|Could not parse content as FormData/i
);
}
});
});
Expand Down
1 change: 0 additions & 1 deletion test/http/extractHttpUserFromHeaders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import 'mocha';
import { HttpRequestUser } from '@azure/functions';
import { expect } from 'chai';
import { Headers } from 'undici';
import { extractHttpUserFromHeaders } from '../../src/http/extractHttpUserFromHeaders';

describe('Extract Http User Claims Principal from Headers', () => {
Expand Down
1 change: 0 additions & 1 deletion types/http.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import { Blob } from 'buffer';
import { ReadableStream } from 'stream/web';
import { BodyInit, FormData, Headers, HeadersInit } from 'undici';
import { URLSearchParams } from 'url';
import { FunctionOptions, FunctionOutput, FunctionResult, FunctionTrigger } from './index';
import { InvocationContext } from './InvocationContext';
Expand Down
Loading