Skip to content

Commit 7e61ae1

Browse files
authored
Support params when streaming (#238)
* Refactor init stream request out of constructor * Support params when streaming And fix headers to match original user's request
1 parent 5561010 commit 7e61ae1

File tree

3 files changed

+64
-53
lines changed

3 files changed

+64
-53
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"@typescript-eslint/no-empty-interface": "off",
2323
"@typescript-eslint/no-explicit-any": "off",
2424
"@typescript-eslint/no-namespace": "off",
25-
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
25+
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "ignoreRestSiblings": true }],
2626
"prefer-const": ["error", { "destructuring": "all" }],
2727
"@typescript-eslint/explicit-member-accessibility": [
2828
"error",

src/InvocationModel.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { toRpcHttp } from './converters/toRpcHttp';
2222
import { toRpcTypedData } from './converters/toRpcTypedData';
2323
import { AzFuncSystemError } from './errors';
2424
import { waitForProxyRequest } from './http/httpProxy';
25-
import { HttpRequest } from './http/HttpRequest';
25+
import { createStreamRequest } from './http/HttpRequest';
2626
import { InvocationContext } from './InvocationContext';
2727
import { isHttpStreamEnabled } from './setup';
2828
import { isHttpTrigger, isTimerTrigger, isTrigger } from './utils/isTrigger';
@@ -78,7 +78,7 @@ export class InvocationModel implements coreTypes.InvocationModel {
7878
let input: unknown;
7979
if (isHttpTrigger(bindingType) && isHttpStreamEnabled()) {
8080
const proxyRequest = await waitForProxyRequest(this.#coreCtx.invocationId);
81-
input = new HttpRequest({ ...binding.data?.http, proxyRequest });
81+
input = createStreamRequest(proxyRequest, nonNullProp(req, 'triggerMetadata'));
8282
} else {
8383
input = fromRpcTypedData(binding.data);
8484
}

src/http/HttpRequest.ts

Lines changed: 61 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,23 @@
33

44
import * as types from '@azure/functions';
55
import { HttpRequestParams, HttpRequestUser } from '@azure/functions';
6-
import { RpcHttpData } from '@azure/functions-core';
6+
import { RpcHttpData, RpcTypedData } from '@azure/functions-core';
77
import { Blob } from 'buffer';
88
import { IncomingMessage } from 'http';
99
import * as stream from 'stream';
1010
import { ReadableStream } from 'stream/web';
11-
import { FormData, Headers, Request as uRequest } from 'undici';
11+
import { FormData, Headers, HeadersInit, Request as uRequest } from 'undici';
1212
import { URLSearchParams } from 'url';
1313
import { fromNullableMapping } from '../converters/fromRpcNullable';
14+
import { fromRpcTypedData } from '../converters/fromRpcTypedData';
1415
import { AzFuncSystemError } from '../errors';
15-
import { nonNullProp } from '../utils/nonNull';
16+
import { isDefined, nonNullProp } from '../utils/nonNull';
1617
import { extractHttpUserFromHeaders } from './extractHttpUserFromHeaders';
1718

1819
interface InternalHttpRequestInit extends RpcHttpData {
1920
undiciRequest?: uRequest;
20-
proxyRequest?: IncomingMessage;
2121
}
2222

23-
type RequestInitResult = [uRequest, URLSearchParams, HttpRequestParams];
24-
2523
export class HttpRequest implements types.HttpRequest {
2624
readonly query: URLSearchParams;
2725
readonly params: HttpRequestParams;
@@ -33,14 +31,6 @@ export class HttpRequest implements types.HttpRequest {
3331
constructor(init: InternalHttpRequestInit) {
3432
this.#init = init;
3533

36-
if (init.proxyRequest) {
37-
[this.#uReq, this.query, this.params] = this.#initStreamRequest(init);
38-
} else {
39-
[this.#uReq, this.query, this.params] = this.#initInMemoryRequest(init);
40-
}
41-
}
42-
43-
#initInMemoryRequest(init: InternalHttpRequestInit): RequestInitResult {
4434
let uReq = init.undiciRequest;
4535
if (!uReq) {
4636
const url = nonNullProp(init, 'url');
@@ -58,45 +48,15 @@ export class HttpRequest implements types.HttpRequest {
5848
headers: fromNullableMapping(init.nullableHeaders, init.headers),
5949
});
6050
}
51+
this.#uReq = uReq;
6152

62-
const query = new URLSearchParams(fromNullableMapping(init.nullableQuery, init.query));
63-
const params = fromNullableMapping(init.nullableParams, init.params);
64-
65-
return [uReq, query, params];
66-
}
67-
68-
#initStreamRequest(init: InternalHttpRequestInit): RequestInitResult {
69-
const proxyReq = nonNullProp(init, 'proxyRequest');
70-
71-
const hostHeaderName = 'x-forwarded-host';
72-
const protoHeaderName = 'x-forwarded-proto';
73-
const host = proxyReq.headers[hostHeaderName];
74-
const proto = proxyReq.headers[protoHeaderName];
75-
if (typeof host !== 'string' || typeof proto !== 'string') {
76-
throw new AzFuncSystemError(`Expected headers "${hostHeaderName}" and "${protoHeaderName}" to be set.`);
77-
}
78-
const url = `${proto}://${host}${nonNullProp(proxyReq, 'url')}`;
79-
80-
let uReq = init.undiciRequest;
81-
if (!uReq) {
82-
let body: stream.Readable | undefined;
83-
const lowerMethod = proxyReq.method?.toLowerCase();
84-
if (lowerMethod !== 'get' && lowerMethod !== 'head') {
85-
body = proxyReq;
86-
}
87-
88-
uReq = new uRequest(url, {
89-
body: body,
90-
duplex: 'half',
91-
method: nonNullProp(proxyReq, 'method'),
92-
headers: <Record<string, string | ReadonlyArray<string>>>proxyReq.headers,
93-
});
53+
if (init.nullableQuery || init.query) {
54+
this.query = new URLSearchParams(fromNullableMapping(init.nullableQuery, init.query));
55+
} else {
56+
this.query = new URL(this.#uReq.url).searchParams;
9457
}
9558

96-
const query = new URL(url).searchParams;
97-
const params = fromNullableMapping(init.nullableParams, init.params);
98-
99-
return [uReq, query, params];
59+
this.params = fromNullableMapping(init.nullableParams, init.params);
10060
}
10161

10262
get url(): string {
@@ -153,3 +113,54 @@ export class HttpRequest implements types.HttpRequest {
153113
return new HttpRequest(newInit);
154114
}
155115
}
116+
117+
export function createStreamRequest(
118+
proxyReq: IncomingMessage,
119+
triggerMetadata: Record<string, RpcTypedData>
120+
): HttpRequest {
121+
const hostHeaderName = 'x-forwarded-host';
122+
const protoHeaderName = 'x-forwarded-proto';
123+
const host = proxyReq.headers[hostHeaderName];
124+
const proto = proxyReq.headers[protoHeaderName];
125+
if (typeof host !== 'string' || typeof proto !== 'string') {
126+
throw new AzFuncSystemError(`Expected headers "${hostHeaderName}" and "${protoHeaderName}" to be set.`);
127+
}
128+
const url = `${proto}://${host}${nonNullProp(proxyReq, 'url')}`;
129+
130+
let body: stream.Readable | undefined;
131+
const lowerMethod = proxyReq.method?.toLowerCase();
132+
if (lowerMethod !== 'get' && lowerMethod !== 'head') {
133+
body = proxyReq;
134+
}
135+
136+
// Get headers and params from trigger metadata
137+
// See here for more info: https://github.com/Azure/azure-functions-host/issues/9840
138+
// NOTE: We ignore query info because it has this bug: https://github.com/Azure/azure-functions-nodejs-library/issues/168
139+
const { Query: rpcQueryIgnored, Headers: rpcHeaders, ...rpcParams } = triggerMetadata;
140+
141+
let headers: HeadersInit | undefined;
142+
const headersData = fromRpcTypedData(rpcHeaders);
143+
if (typeof headersData === 'object' && isDefined(headersData)) {
144+
headers = <HeadersInit>headersData;
145+
}
146+
147+
const uReq = new uRequest(url, {
148+
body,
149+
duplex: 'half',
150+
method: nonNullProp(proxyReq, 'method'),
151+
headers,
152+
});
153+
154+
const params: Record<string, string> = {};
155+
for (const [key, rpcValue] of Object.entries(rpcParams)) {
156+
const value = fromRpcTypedData(rpcValue);
157+
if (typeof value === 'string') {
158+
params[key] = value;
159+
}
160+
}
161+
162+
return new HttpRequest({
163+
undiciRequest: uReq,
164+
params,
165+
});
166+
}

0 commit comments

Comments
 (0)