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
22 changes: 20 additions & 2 deletions packages/net/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ export async function fetch(
cache: "no-cache",
};

// Skip caching for POST and PATCH requests
if (options.method === "POST" || options.method === "PATCH") {
// Skip caching for POST, PATCH, and HEAD requests
if (
options.method === "POST" ||
options.method === "PATCH" ||
options.method === "HEAD"
) {
const response = await undiciFetch(url, fetchOptions);
/* c8 ignore next 3 */
if (!response.ok) {
Expand Down Expand Up @@ -239,5 +243,19 @@ export async function patch<T = unknown>(
};
}

/**
* Perform a HEAD request to a URL with optional request options.
* @param {string} url The URL to fetch.
* @param {Omit<FetchOptions, 'method'>} options Optional request options. The `cache` property is required.
* @returns {Promise<UndiciResponse>} The response from the fetch (no body).
*/
export async function head(
url: string,
options: Omit<FetchOptions, "method">,
): Promise<UndiciResponse> {
const response = await fetch(url, { ...options, method: "HEAD" });
return response;
}

export type Response = UndiciResponse;
export type { RequestInit as FetchRequestInit } from "undici";
15 changes: 15 additions & 0 deletions packages/net/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,20 @@ export class CacheableNet extends Hookified {
};
}

/**
* Perform a HEAD request to a URL with optional request options. Will use the cache that is already set in the instance.
* @param {string} url The URL to fetch.
* @param {Omit<FetchRequestInit, 'method'>} options Optional request options (method will be set to HEAD).
* @returns {Promise<FetchResponse>} The response from the fetch (no body).
*/
public async head(
url: string,
options?: Omit<FetchRequestInit, "method">,
): Promise<FetchResponse> {
const response = await this.fetch(url, { ...options, method: "HEAD" });
return response;
}

/**
* Perform a PATCH request to a URL with data and optional request options. Will use the cache that is already set in the instance.
* @param {string} url The URL to fetch.
Expand Down Expand Up @@ -219,6 +233,7 @@ export {
fetch,
type GetResponse,
get,
head,
patch,
post,
type Response as FetchResponse,
Expand Down
80 changes: 67 additions & 13 deletions packages/net/test/fetch.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import process from "node:process";
import { Cacheable } from "cacheable";
import { describe, expect, test } from "vitest";
import { type FetchOptions, fetch, get, patch, post } from "../src/fetch.js";
import {
type FetchOptions,
fetch,
get,
head,
patch,
post,
} from "../src/fetch.js";

const testUrl = process.env.TEST_URL ?? "https://mockhttp.org";
const testTimeout = 10_000; // 10 seconds
Expand Down Expand Up @@ -120,15 +127,16 @@ describe("Fetch", () => {
"should handle non-JSON response in get helper",
async () => {
const cache = new Cacheable();
// Mock a text response by using a URL that returns plain text
const mockTextUrl = "https://httpbin.org/robots.txt";
// Use mockhttp.org/plain which returns plain text
const url = `${testUrl}/plain`;
const options = {
cache,
};
const result = await get(mockTextUrl, options);
const result = await get(url, options);
expect(result).toBeDefined();
expect(result.data).toBeDefined();
expect(typeof result.data).toBe("string");
expect(result.data).toBeTruthy(); // Plain text is not empty
expect(result.response).toBeDefined();
expect(result.response.status).toBe(200);
},
Expand Down Expand Up @@ -175,23 +183,65 @@ describe("Fetch", () => {
testTimeout,
);

test(
"should fetch data using head helper",
async () => {
const url = `${testUrl}/get`;
const options = {
cache: new Cacheable(),
};
const response = await head(url, options);
expect(response).toBeDefined();
expect(response.status).toBe(200);
// Headers should still be present
expect(response.headers).toBeDefined();
},
testTimeout,
);

test(
"should not cache HEAD requests (HEAD requests are not cached)",
async () => {
const cache = new Cacheable({ stats: true });
const url = `${testUrl}/get`;
const options = {
cache,
};
const response1 = await head(url, options);
const response2 = await head(url, options);
expect(response1).toBeDefined();
expect(response2).toBeDefined();
expect(cache.stats).toBeDefined();
// HEAD requests should not be cached, so expect 0 hits
expect(cache.stats.hits).toBe(0);
// Both responses should have the same status
expect(response1.status).toBe(200);
expect(response2.status).toBe(200);
},
testTimeout,
);

test(
"should handle non-JSON response in post helper",
async () => {
const cache = new Cacheable();
// Use httpbin's status endpoint that accepts POST and returns non-JSON
const url = "https://httpbin.org/status/201";
// Use mockhttp.org/plain which now accepts POST and returns plain text
const url = `${testUrl}/plain`;
const data = "test data";
const options = {
cache,
headers: {
"Content-Type": "text/plain",
},
};
const result = await post(url, data, options);
expect(result).toBeDefined();
// Status endpoint returns empty body, which will be parsed as empty string
expect(result.data).toBe("");
// The plain endpoint returns text, which should be returned as a string
expect(result.data).toBeDefined();
expect(typeof result.data).toBe("string");
expect(result.data).toBeTruthy(); // Plain text is not empty
expect(result.response).toBeDefined();
expect(result.response.status).toBe(201);
expect(result.response.status).toBe(200);
},
testTimeout,
);
Expand Down Expand Up @@ -240,17 +290,21 @@ describe("Fetch", () => {
"should handle non-JSON response in patch helper",
async () => {
const cache = new Cacheable();
// Use httpbin's status endpoint that accepts PATCH and returns non-JSON
const url = "https://httpbin.org/status/200";
// Use mockhttp.org/plain which now accepts PATCH and returns plain text
const url = `${testUrl}/plain`;
const data = "test data";
const options = {
cache,
headers: {
"Content-Type": "text/plain",
},
};
const result = await patch(url, data, options);
expect(result).toBeDefined();
// Status endpoint returns empty body
expect(result.data).toBe("");
// The plain endpoint returns text, which should be returned as a string
expect(result.data).toBeDefined();
expect(typeof result.data).toBe("string");
expect(result.data).toBeTruthy(); // Plain text is not empty
expect(result.response).toBeDefined();
expect(result.response.status).toBe(200);
},
Expand Down
89 changes: 75 additions & 14 deletions packages/net/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type FetchOptions,
fetch,
get,
head,
Net,
patch,
post,
Expand Down Expand Up @@ -138,12 +139,13 @@ describe("Cacheable Net", () => {
"should handle non-JSON response in CacheableNet get method",
async () => {
const net = new Net();
// Use a URL that returns plain text
const mockTextUrl = "https://httpbin.org/robots.txt";
const result = await net.get(mockTextUrl);
// Use mockhttp.org/plain which returns plain text
const url = `${testUrl}/plain`;
const result = await net.get(url);
expect(result).toBeDefined();
expect(result.data).toBeDefined();
expect(typeof result.data).toBe("string");
expect(result.data).toBeTruthy(); // Plain text is not empty
expect(result.response).toBeDefined();
expect(result.response.status).toBe(200);
},
Expand Down Expand Up @@ -216,20 +218,74 @@ describe("Cacheable Net", () => {
testTimeout,
);

test(
"should fetch data using CacheableNet head method",
async () => {
const net = new Net();
const url = `${testUrl}/get`;
const response = await net.head(url);
expect(response).toBeDefined();
expect(response.status).toBe(200);
// Headers should still be present
expect(response.headers).toBeDefined();
},
testTimeout,
);

test(
"should fetch data using standalone head function",
async () => {
const url = `${testUrl}/get`;
const options = {
cache: new Cacheable(),
};
const response = await head(url, options);
expect(response).toBeDefined();
expect(response.status).toBe(200);
// Headers should still be present
expect(response.headers).toBeDefined();
},
testTimeout,
);

test(
"should handle head with options in CacheableNet",
async () => {
const net = new Net();
const url = `${testUrl}/get`;
const options = {
headers: {
"User-Agent": "test-agent",
},
};
const response = await net.head(url, options);
expect(response).toBeDefined();
expect(response.status).toBe(200);
// Headers should still be present
expect(response.headers).toBeDefined();
},
testTimeout,
);

test(
"should handle non-JSON response in CacheableNet post method",
async () => {
const net = new Net();
// Use httpbin's status endpoint that returns non-JSON
const url = "https://httpbin.org/status/201";
// Use mockhttp.org/plain which now accepts POST and returns plain text
const url = `${testUrl}/plain`;
const data = "test data";
const result = await net.post(url, data);
const result = await net.post(url, data, {
headers: {
"Content-Type": "text/plain",
},
});
expect(result).toBeDefined();
// Status endpoint returns empty body
expect(result.data).toBe("");
// The plain endpoint returns text, which should be returned as a string
expect(result.data).toBeDefined();
expect(typeof result.data).toBe("string");
expect(result.data).toBeTruthy(); // Plain text is not empty
expect(result.response).toBeDefined();
expect(result.response.status).toBe(201);
expect(result.response.status).toBe(200);
},
testTimeout,
);
Expand Down Expand Up @@ -292,14 +348,19 @@ describe("Cacheable Net", () => {
"should handle non-JSON response in CacheableNet patch method",
async () => {
const net = new Net();
// Use httpbin's status endpoint that returns non-JSON
const url = "https://httpbin.org/status/200";
// Use mockhttp.org/plain which now accepts PATCH and returns plain text
const url = `${testUrl}/plain`;
const data = "test data";
const result = await net.patch(url, data);
const result = await net.patch(url, data, {
headers: {
"Content-Type": "text/plain",
},
});
expect(result).toBeDefined();
// Status endpoint returns empty body
expect(result.data).toBe("");
// The plain endpoint returns text, which should be returned as a string
expect(result.data).toBeDefined();
expect(typeof result.data).toBe("string");
expect(result.data).toBeTruthy(); // Plain text is not empty
expect(result.response).toBeDefined();
expect(result.response.status).toBe(200);
},
Expand Down