Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Readable } from "node:stream";
import destr from "destr";
import { withBase, withQuery } from "ufo";
import { withBase, withQuery } from "./path";
import { createFetchError } from "./error";
import {
isPayloadMethod,
Expand Down Expand Up @@ -186,7 +186,7 @@
context.options.onRequestError
);
}
return await onError(context);

Check failure on line 189 in src/fetch.ts

View workflow job for this annotation

GitHub Actions / ci (18)

test/index.test.ts > ofetch > deep merges defaultOptions

FetchError: [GET] "/:3000/echo?b=2&a=1&c=3": <no response> Failed to parse URL from /:3000/echo?b=2&a=1&c=3 ❯ $fetchRaw2 src/fetch.ts:189:14 ❯ $fetch2 src/fetch.ts:250:15 ❯ test/index.test.ts:366:31 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { request: '/:3000/echo?b=2&a=1&c=3', options: { query: { b: 2, a: 1, c: 3 }, params: { b: 2, a: 1, c: 3 }, headers: { constructor: 'Function<Headers>', append: 'Function<append>', delete: 'Function<delete>', get: 'Function<get>', has: 'Function<has>', set: 'Function<set>', getSetCookie: 'Function<getSetCookie>', keys: 'Function<keys>', values: 'Function<values>', entries: 'Function<entries>', forEach: 'Function<forEach>' }, method: undefined, retry: +0 }, response: undefined, data: undefined, status: undefined, statusCode: undefined, statusText: undefined, statusMessage: undefined } Caused by: Caused by: TypeError: Failed to parse URL from /:3000/echo?b=2&a=1&c=3 Caused by: Caused by: TypeError: Invalid URL ❯ src/node.ts:38:66 ❯ $fetchRaw2 src/fetch.ts:177:32 ❯ onError src/fetch.ts:77:16 ❯ $fetchRaw2 src/fetch.ts:189:20 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { input: '/:3000/echo?b=2&a=1&c=3', code: 'ERR_INVALID_URL' }

Check failure on line 189 in src/fetch.ts

View workflow job for this annotation

GitHub Actions / ci (20)

test/index.test.ts > ofetch > deep merges defaultOptions

FetchError: [GET] "/:3000/echo?b=2&a=1&c=3": <no response> Failed to parse URL from /:3000/echo?b=2&a=1&c=3 ❯ $fetchRaw2 src/fetch.ts:189:14 ❯ $fetch2 src/fetch.ts:250:15 ❯ test/index.test.ts:366:31 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { request: '/:3000/echo?b=2&a=1&c=3', options: { query: { b: 2, a: 1, c: 3 }, params: { b: 2, a: 1, c: 3 }, headers: { constructor: 'Function<Headers>', append: 'Function<append>', delete: 'Function<delete>', get: 'Function<get>', has: 'Function<has>', set: 'Function<set>', getSetCookie: 'Function<getSetCookie>', keys: 'Function<keys>', values: 'Function<values>', entries: 'Function<entries>', forEach: 'Function<forEach>' }, method: undefined, retry: +0 }, response: undefined, data: undefined, status: undefined, statusCode: undefined, statusText: undefined, statusMessage: undefined } Caused by: Caused by: TypeError: Failed to parse URL from /:3000/echo?b=2&a=1&c=3 ❯ $fetchRaw2 src/fetch.ts:177:26 Caused by: Caused by: TypeError: Invalid URL ❯ src/node.ts:38:66 ❯ $fetchRaw2 src/fetch.ts:177:32 ❯ onError src/fetch.ts:77:16 ❯ $fetchRaw2 src/fetch.ts:189:20 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_INVALID_URL', input: '/:3000/echo?b=2&a=1&c=3' }

Check failure on line 189 in src/fetch.ts

View workflow job for this annotation

GitHub Actions / ci (22)

test/index.test.ts > ofetch > deep merges defaultOptions

FetchError: [GET] "/:3000/echo?b=2&a=1&c=3": <no response> Failed to parse URL from /:3000/echo?b=2&a=1&c=3 ❯ $fetchRaw2 src/fetch.ts:189:14 ❯ $fetch2 src/fetch.ts:250:15 ❯ test/index.test.ts:366:31 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { request: '/:3000/echo?b=2&a=1&c=3', options: { query: { b: 2, a: 1, c: 3 }, params: { b: 2, a: 1, c: 3 }, headers: { constructor: 'Function<Headers>', append: 'Function<append>', delete: 'Function<delete>', get: 'Function<get>', has: 'Function<has>', set: 'Function<set>', getSetCookie: 'Function<getSetCookie>', keys: 'Function<keys>', values: 'Function<values>', entries: 'Function<entries>', forEach: 'Function<forEach>' }, method: undefined, retry: +0 }, response: undefined, data: undefined, status: undefined, statusCode: undefined, statusText: undefined, statusMessage: undefined } Caused by: Caused by: TypeError: Failed to parse URL from /:3000/echo?b=2&a=1&c=3 ❯ $fetchRaw2 src/fetch.ts:177:26 Caused by: Caused by: TypeError: Invalid URL ❯ src/node.ts:38:66 ❯ $fetchRaw2 src/fetch.ts:177:32 ❯ onError src/fetch.ts:77:16 ❯ $fetchRaw2 src/fetch.ts:189:20 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_INVALID_URL', input: '/:3000/echo?b=2&a=1&c=3' }
} finally {
if (abortTimeout) {
clearTimeout(abortTimeout);
Expand Down
108 changes: 108 additions & 0 deletions src/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* eslint-disable unicorn/prefer-at */
export type QueryValue =
| string
| number
| boolean
| QueryValue[]
| Record<string, any>
| null
| undefined;
export type QueryObject = Record<string, QueryValue | QueryValue[]>;

/**
* Removes the leading slash from the given path if it has one.
*/
export function withoutLeadingSlash(path?: string): string {
if (!path || path === "/") {
return "/";
}

return path[0] === "/" ? path.slice(1) : path;
}

/**
* Removes the trailing slash from the given path if it has one.
*/
export function withoutTrailingSlash(path?: string): string {
if (!path || path === "/") {
return "/";
}

return path[path.length - 1] === "/" ? path.slice(0, -1) : path;
}

/**
* Joins the given base URL and path, ensuring that there is only one slash between them.
*/
export function joinURL(base?: string, path?: string): string {
if (!base || base === "/") {
return path || "/";
}

if (!path || path === "/") {
return base || "/";
}

const baseHasTrailing = base[base.length - 1] === "/";
const pathHasLeading = path[0] === "/";
if (baseHasTrailing && pathHasLeading) {
return base + path.slice(1);
}

if (!baseHasTrailing && !pathHasLeading) {
return `${base}/${path}`;
}

return base + path;
}

/**
* Adds the base path to the input path, if it is not already present.
*/
export function withBase(input = "", base = ""): string {
if (!base || base === "/") {
return input;
}

const _base = withoutTrailingSlash(base);
if (input.startsWith(_base)) {
return input;
}

return joinURL(_base, input);
}

/**
* Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
*/
export function withQuery(input: string, query: QueryObject): string {
const url = new URL(input, "http://localhost");
const searchParams = new URLSearchParams(url.search);

for (const [key, value] of Object.entries(query)) {
if (value === undefined) {
searchParams.delete(key);
} else if (typeof value === "number" || typeof value === "boolean") {
searchParams.set(key, String(value));
} else if (!value) {
searchParams.set(key, "");
} else if (Array.isArray(value)) {
for (const item of value) {
searchParams.append(key, String(item));
}
} else if (typeof value === "object") {
searchParams.set(key, JSON.stringify(value));
} else {
searchParams.set(key, String(value));
}
}

url.search = searchParams.toString();
let urlWithQuery = url.toString();

if (urlWithQuery.startsWith("http://localhost")) {
urlWithQuery = urlWithQuery.slice(16);
}

return urlWithQuery;
}
Loading