//#region src/utils.url.ts
/**
* Joins the given base URL and path, ensuring that there is only one slash between them.
*/
function joinURL(base, path) {
	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.
*/
function withBase(input = "", base = "") {
	if (!base || base === "/") return input;
	const _base = withoutTrailingSlash(base);
	if (input.startsWith(_base)) return input;
	return joinURL(_base, input);
}
function withoutTrailingSlash(path) {
	if (!path || path === "/") return "/";
	return path[path.length - 1] === "/" ? path.slice(0, -1) : path;
}
/**
* Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
*/
function withQuery(input, query) {
	if (!query || Object.keys(query).length === 0) return input;
	const searchIndex = input.indexOf("?");
	if (searchIndex === -1) {
		const normalizedQuery = Object.entries(query).filter(([, value]) => value !== void 0).flatMap(([key, value]) => {
			if (Array.isArray(value)) return value.map((item) => [key, normalizeQueryValue(item)]);
			return [[key, normalizeQueryValue(value)]];
		});
		const queryString$1 = new URLSearchParams(normalizedQuery).toString();
		return queryString$1 ? `${input}?${queryString$1}` : input;
	}
	const searchParams = new URLSearchParams(input.slice(searchIndex + 1));
	const base = input.slice(0, searchIndex);
	for (const [key, value] of Object.entries(query)) if (value === void 0) searchParams.delete(key);
	else if (Array.isArray(value)) for (const item of value) searchParams.append(key, normalizeQueryValue(item));
	else searchParams.set(key, normalizeQueryValue(value));
	const queryString = searchParams.toString();
	return queryString ? `${base}?${queryString}` : base;
}
function normalizeQueryValue(value) {
	if (value === null) return "";
	if (typeof value === "number" || typeof value === "boolean") return String(value);
	if (typeof value === "object") return JSON.stringify(value);
	return String(value);
}

//#endregion
//#region src/error.ts
var FetchError = class extends Error {
	constructor(message, opts) {
		super(message, opts);
		this.name = "FetchError";
		if (opts?.cause && !this.cause) this.cause = opts.cause;
	}
};
function createFetchError(ctx) {
	const errorMessage = ctx.error?.message || ctx.error?.toString() || "";
	const method = ctx.request?.method || ctx.options?.method || "GET";
	const url = ctx.request?.url || String(ctx.request) || "/";
	const fetchError = new FetchError(`${`[${method}] ${JSON.stringify(url)}`}: ${ctx.response ? `${ctx.response.status} ${ctx.response.statusText}` : "<no response>"}${errorMessage ? ` ${errorMessage}` : ""}`, ctx.error ? { cause: ctx.error } : void 0);
	for (const key of [
		"request",
		"options",
		"response"
	]) Object.defineProperty(fetchError, key, { get() {
		return ctx[key];
	} });
	for (const [key, refKey] of [
		["data", "_data"],
		["status", "status"],
		["statusCode", "status"],
		["statusText", "statusText"],
		["statusMessage", "statusText"]
	]) Object.defineProperty(fetchError, key, { get() {
		return ctx.response && ctx.response[refKey];
	} });
	return fetchError;
}

//#endregion
//#region src/utils.ts
const payloadMethods = new Set(Object.freeze([
	"PATCH",
	"POST",
	"PUT",
	"DELETE"
]));
function isPayloadMethod(method = "GET") {
	return payloadMethods.has(method.toUpperCase());
}
function isJSONSerializable(value) {
	if (value === void 0) return false;
	const t = typeof value;
	if (t === "string" || t === "number" || t === "boolean" || t === null) return true;
	if (t !== "object") return false;
	if (Array.isArray(value)) return true;
	if (value.buffer) return false;
	if (value instanceof FormData || value instanceof URLSearchParams) return false;
	return value.constructor && value.constructor.name === "Object" || typeof value.toJSON === "function";
}
const textTypes = new Set([
	"image/svg",
	"application/xml",
	"application/xhtml",
	"application/html"
]);
const JSON_RE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i;
function detectResponseType(_contentType = "") {
	if (!_contentType) return "json";
	const contentType = _contentType.split(";").shift() || "";
	if (JSON_RE.test(contentType)) return "json";
	if (contentType === "text/event-stream") return "stream";
	if (textTypes.has(contentType) || contentType.startsWith("text/")) return "text";
	return "blob";
}
function resolveFetchOptions(request, input, defaults, Headers$1) {
	const headers = mergeHeaders(input?.headers ?? request?.headers, defaults?.headers, Headers$1);
	let query;
	if (defaults?.query || defaults?.params || input?.params || input?.query) query = {
		...defaults?.params,
		...defaults?.query,
		...input?.params,
		...input?.query
	};
	return {
		...defaults,
		...input,
		query,
		params: query,
		headers
	};
}
function mergeHeaders(input, defaults, Headers$1) {
	if (!defaults) return new Headers$1(input);
	const headers = new Headers$1(defaults);
	if (input) for (const [key, value] of Symbol.iterator in input || Array.isArray(input) ? input : new Headers$1(input)) headers.set(key, value);
	return headers;
}
async function callHooks(context, hooks) {
	if (hooks) if (Array.isArray(hooks)) for (const hook of hooks) await hook(context);
	else await hooks(context);
}

//#endregion
//#region src/fetch.ts
const retryStatusCodes = new Set([
	408,
	409,
	425,
	429,
	500,
	502,
	503,
	504
]);
const nullBodyResponses = new Set([
	101,
	204,
	205,
	304
]);
function createFetch(globalOptions = {}) {
	const { fetch: fetch$1 = globalThis.fetch } = globalOptions;
	async function onError(context) {
		const isAbort = context.error && context.error.name === "AbortError" && !context.options.timeout || false;
		if (context.options.retry !== false && !isAbort) {
			let retries;
			if (typeof context.options.retry === "number") retries = context.options.retry;
			else retries = isPayloadMethod(context.options.method) ? 0 : 1;
			const responseCode = context.response && context.response.status || 500;
			if (retries > 0 && (Array.isArray(context.options.retryStatusCodes) ? context.options.retryStatusCodes.includes(responseCode) : retryStatusCodes.has(responseCode))) {
				const retryDelay = typeof context.options.retryDelay === "function" ? context.options.retryDelay(context) : context.options.retryDelay || 0;
				if (retryDelay > 0) await new Promise((resolve) => setTimeout(resolve, retryDelay));
				return $fetchRaw(context.request, {
					...context.options,
					retry: retries - 1
				});
			}
		}
		const error = createFetchError(context);
		if (Error.captureStackTrace) Error.captureStackTrace(error, $fetchRaw);
		throw error;
	}
	const $fetchRaw = async function $fetchRaw$1(_request, _options = {}) {
		const context = {
			request: _request,
			options: resolveFetchOptions(_request, _options, globalOptions.defaults, Headers),
			response: void 0,
			error: void 0
		};
		if (context.options.method) context.options.method = context.options.method.toUpperCase();
		if (context.options.onRequest) await callHooks(context, context.options.onRequest);
		if (typeof context.request === "string") {
			if (context.options.baseURL) context.request = withBase(context.request, context.options.baseURL);
			if (context.options.query) {
				context.request = withQuery(context.request, context.options.query);
				delete context.options.query;
			}
			if ("query" in context.options) delete context.options.query;
			if ("params" in context.options) delete context.options.params;
		}
		if (context.options.body && isPayloadMethod(context.options.method)) {
			if (isJSONSerializable(context.options.body)) {
				const contentType = context.options.headers.get("content-type");
				if (typeof context.options.body !== "string") context.options.body = contentType === "application/x-www-form-urlencoded" ? new URLSearchParams(context.options.body).toString() : JSON.stringify(context.options.body);
				context.options.headers = new Headers(context.options.headers || {});
				if (!contentType) context.options.headers.set("content-type", "application/json");
				if (!context.options.headers.has("accept")) context.options.headers.set("accept", "application/json");
			} else if ("pipeTo" in context.options.body && typeof context.options.body.pipeTo === "function" || typeof context.options.body.pipe === "function") {
				if (!("duplex" in context.options)) context.options.duplex = "half";
			}
		}
		if (context.options.timeout) context.options.signal = context.options.signal ? AbortSignal.any([AbortSignal.timeout(context.options.timeout), context.options.signal]) : AbortSignal.timeout(context.options.timeout);
		try {
			context.response = await fetch$1(context.request, context.options);
		} catch (error) {
			context.error = error;
			if (context.options.onRequestError) await callHooks(context, context.options.onRequestError);
			return await onError(context);
		}
		if ((context.response.body || context.response._bodyInit) && !nullBodyResponses.has(context.response.status) && context.options.method !== "HEAD") {
			const responseType = (context.options.parseResponse ? "json" : context.options.responseType) || detectResponseType(context.response.headers.get("content-type") || "");
			switch (responseType) {
				case "json": {
					const data = await context.response.text();
					if (data) {
						const parseFunction = context.options.parseResponse || JSON.parse;
						context.response._data = parseFunction(data);
					}
					break;
				}
				case "stream":
					context.response._data = context.response.body || context.response._bodyInit;
					break;
				default: context.response._data = await context.response[responseType]();
			}
		}
		if (context.options.onResponse) await callHooks(context, context.options.onResponse);
		if (!context.options.ignoreResponseError && context.response.status >= 400 && context.response.status < 600) {
			if (context.options.onResponseError) await callHooks(context, context.options.onResponseError);
			return await onError(context);
		}
		return context.response;
	};
	const $fetch$1 = async function $fetch$2(request, options) {
		return (await $fetchRaw(request, options))._data;
	};
	$fetch$1.raw = $fetchRaw;
	$fetch$1.native = (...args) => fetch$1(...args);
	$fetch$1.create = (defaultOptions = {}, customGlobalOptions = {}) => createFetch({
		...globalOptions,
		...customGlobalOptions,
		defaults: {
			...globalOptions.defaults,
			...customGlobalOptions.defaults,
			...defaultOptions
		}
	});
	return $fetch$1;
}

//#endregion
//#region src/index.ts
const fetch = globalThis.fetch ? (...args) => globalThis.fetch(...args) : () => Promise.reject(/* @__PURE__ */ new Error("[ofetch] global.fetch is not supported!"));
const ofetch = createFetch({ fetch });
const $fetch = ofetch;

//#endregion
export { $fetch, FetchError, createFetch, createFetchError, fetch, ofetch };