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 src/http/HttpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,10 @@ export function createStreamRequest(
// NOTE: We ignore query info because it has this bug: https://github.com/Azure/azure-functions-nodejs-library/issues/168
const { Query: rpcQueryIgnored, Headers: rpcHeaders, ...rpcParams } = triggerMetadata;

let headers: HeadersInit | undefined;
let headers: types.HttpHeadersInit | undefined;
const headersData = fromRpcTypedData(rpcHeaders);
if (typeof headersData === 'object' && isDefined(headersData)) {
headers = <HeadersInit>headersData;
headers = <types.HttpHeadersInit>headersData;
}

const nativeReq = new Request(url, {
Expand Down
4 changes: 3 additions & 1 deletion src/http/HttpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export class HttpResponse implements types.HttpResponse {
}
this.#nativeRes = new Response(jsonBody, { ...resInit, headers: jsonHeaders });
} else {
this.#nativeRes = new Response(init.body, resInit);
// Cast to BodyInit to satisfy the native Response constructor
// Our HttpResponseBodyInit type is compatible with what Node.js accepts
this.#nativeRes = new Response(init.body as BodyInit, resInit);
}
}

Expand Down
227 changes: 227 additions & 0 deletions test/http/HttpResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// Licensed under the MIT License.

import 'mocha';
import { Blob } from 'buffer';
import * as chai from 'chai';
import { expect } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { ReadableStream } from 'stream/web';
import { HttpResponse } from '../../src/http/HttpResponse';

chai.use(chaiAsPromised);
Expand Down Expand Up @@ -33,4 +35,229 @@ describe('HttpResponse', () => {
expect(res.cookies).to.not.equal(res2.cookies);
expect(res.cookies).to.deep.equal(res2.cookies);
});

describe('HttpResponseBodyInit types', () => {
it('string body', async () => {
const res = new HttpResponse({
body: 'Hello World',
});
expect(await res.text()).to.equal('Hello World');
});

it('null body', () => {
const res = new HttpResponse({
body: null,
});
expect(res.body).to.be.null;
});

it('undefined body', () => {
const res = new HttpResponse({
body: undefined,
});
expect(res.body).to.be.null;
});

it('ArrayBuffer body', async () => {
const encoder = new TextEncoder();
const buffer = encoder.encode('ArrayBuffer content').buffer;
const res = new HttpResponse({
body: buffer,
});
expect(await res.text()).to.equal('ArrayBuffer content');
});

it('Uint8Array (ArrayBufferView) body', async () => {
const encoder = new TextEncoder();
const uint8Array = encoder.encode('Uint8Array content');
const res = new HttpResponse({
body: uint8Array,
});
expect(await res.text()).to.equal('Uint8Array content');
});

it('Blob body', async () => {
const blob = new Blob(['Blob content'], { type: 'text/plain' });
const res = new HttpResponse({
body: blob,
});
expect(await res.text()).to.equal('Blob content');
});

it('ReadableStream body', async () => {
// Create a ReadableStream with properly encoded Uint8Array chunks
const encoder = new TextEncoder();
const chunks = [encoder.encode('Stream '), encoder.encode('content')];
let index = 0;

const webStream = new ReadableStream({
pull(controller) {
if (index < chunks.length) {
controller.enqueue(chunks[index++]);
} else {
controller.close();
}
},
});

const res = new HttpResponse({
body: webStream,
});
expect(await res.text()).to.equal('Stream content');
});
});

describe('HttpHeadersInit types', () => {
it('Record<string, string> headers', () => {
const res = new HttpResponse({
body: 'test',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'custom-value',
},
});
expect(res.headers.get('Content-Type')).to.equal('application/json');
expect(res.headers.get('X-Custom-Header')).to.equal('custom-value');
});

it('Array of tuples headers', () => {
const res = new HttpResponse({
body: 'test',
headers: [
['Content-Type', 'text/plain'],
['X-Request-Id', '12345'],
],
});
expect(res.headers.get('Content-Type')).to.equal('text/plain');
expect(res.headers.get('X-Request-Id')).to.equal('12345');
});

it('Headers class', () => {
const headers = new Headers();
headers.set('Content-Type', 'text/html');
headers.set('Cache-Control', 'no-cache');

const res = new HttpResponse({
body: 'test',
headers: headers,
});
expect(res.headers.get('Content-Type')).to.equal('text/html');
expect(res.headers.get('Cache-Control')).to.equal('no-cache');
});

it('undefined headers', () => {
const res = new HttpResponse({
body: 'test',
headers: undefined,
});
expect(res.headers).to.not.be.undefined;
});

it('empty Record headers', () => {
const res = new HttpResponse({
body: 'test',
headers: {},
});
expect(res.headers).to.not.be.undefined;
});

it('empty array headers', () => {
const res = new HttpResponse({
body: 'test',
headers: [],
});
expect(res.headers).to.not.be.undefined;
});
});

describe('combined body and headers', () => {
it('json body with proper content-type header', async () => {
const res = new HttpResponse({
jsonBody: { message: 'Hello', count: 42 },
headers: {
'X-Custom': 'value',
},
});
expect(res.headers.get('Content-Type')).to.equal('application/json');
expect(res.headers.get('X-Custom')).to.equal('value');
expect(await res.json()).to.deep.equal({ message: 'Hello', count: 42 });
});

it('ArrayBuffer body with tuple headers', async () => {
const encoder = new TextEncoder();
const buffer = encoder.encode('binary data').buffer;
const res = new HttpResponse({
body: buffer,
headers: [
['Content-Type', 'application/octet-stream'],
['Content-Length', '11'],
],
});
expect(res.headers.get('Content-Type')).to.equal('application/octet-stream');
expect(await res.text()).to.equal('binary data');
});

it('Blob body with Headers class', async () => {
const blob = new Blob(['test data'], { type: 'text/plain' });
const headers = new Headers();
headers.set('X-Blob-Size', '9');

const res = new HttpResponse({
body: blob,
headers: headers,
});
expect(res.headers.get('X-Blob-Size')).to.equal('9');
expect(await res.text()).to.equal('test data');
});

it('FormData body', async () => {
const formData = new FormData();
formData.append('field1', 'value1');
formData.append('field2', 'value2');

const res = new HttpResponse({
body: formData,
});

// FormData body should set multipart/form-data content-type
const contentType = res.headers.get('Content-Type');
expect(contentType).to.include('multipart/form-data');

// Verify we can get the FormData back
const responseFormData = await res.formData();
expect(responseFormData.get('field1')).to.equal('value1');
expect(responseFormData.get('field2')).to.equal('value2');
});

it('URLSearchParams body', async () => {
const params = new URLSearchParams();
params.append('key1', 'value1');
params.append('key2', 'value2');

const res = new HttpResponse({
body: params,
});

// URLSearchParams should set application/x-www-form-urlencoded content-type
const contentType = res.headers.get('Content-Type');
expect(contentType).to.equal('application/x-www-form-urlencoded;charset=UTF-8');

// Verify the body text
expect(await res.text()).to.equal('key1=value1&key2=value2');
});

it('Map as headers (converted to array)', () => {
const headersMap = new Map<string, string>([
['Content-Type', 'application/json'],
['X-Custom-Header', 'map-value'],
]);

const res = new HttpResponse({
body: 'test',
headers: [...headersMap],
});
expect(res.headers.get('Content-Type')).to.equal('application/json');
expect(res.headers.get('X-Custom-Header')).to.equal('map-value');
});
});
});
27 changes: 25 additions & 2 deletions types/http.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ import { URLSearchParams } from 'url';
import { FunctionOptions, FunctionOutput, FunctionResult, FunctionTrigger } from './index';
import { InvocationContext } from './InvocationContext';

/**
* Represents the body types that can be used in an HTTP response.
* This is a local definition to avoid dependency on lib.dom.
* Compatible with Node.js native Fetch API body types.
*/
export type HttpResponseBodyInit =
| ReadableStream
| Blob
| ArrayBufferView
| ArrayBuffer
| FormData
| URLSearchParams
| string
| null
| undefined;

/**
* Represents the headers types that can be used to initialize HTTP headers.
* This is a local definition to avoid dependency on lib.dom.
* Compatible with Node.js native Fetch API header types.
*/
export type HttpHeadersInit = Headers | Record<string, string> | [string, string][];

export type HttpHandler = (
request: HttpRequest,
context: InvocationContext
Expand Down Expand Up @@ -203,7 +226,7 @@ export interface HttpResponseInit {
/**
* HTTP response body
*/
body?: BodyInit;
body?: HttpResponseBodyInit;

/**
* A JSON-serializable HTTP Response body.
Expand All @@ -220,7 +243,7 @@ export interface HttpResponseInit {
/**
* HTTP response headers
*/
headers?: HeadersInit;
headers?: HttpHeadersInit;

/**
* HTTP response cookies
Expand Down
Loading