diff --git a/.changeset/calm-methods-webdav.md b/.changeset/calm-methods-webdav.md new file mode 100644 index 00000000000..06dd2272bf4 --- /dev/null +++ b/.changeset/calm-methods-webdav.md @@ -0,0 +1,6 @@ +--- +"@effect/platform": patch +"@effect/ai": patch +--- + +Support WebDAV and other uncommon HTTP methods. diff --git a/packages/ai/ai/src/AiError.ts b/packages/ai/ai/src/AiError.ts index f87687ea291..7416980617c 100644 --- a/packages/ai/ai/src/AiError.ts +++ b/packages/ai/ai/src/AiError.ts @@ -70,6 +70,7 @@ * @since 1.0.0 */ import type * as HttpClientError from "@effect/platform/HttpClientError" +import * as HttpMethod from "@effect/platform/HttpMethod" import * as Effect from "effect/Effect" import * as Inspectable from "effect/Inspectable" import type { ParseError } from "effect/ParseResult" @@ -145,7 +146,7 @@ export const isAiError = (u: unknown): u is AiError => Predicate.hasProperty(u, * @category Schemas */ export const HttpRequestDetails = Schema.Struct({ - method: Schema.Literal("GET", "POST", "PATCH", "PUT", "DELETE", "HEAD", "OPTIONS"), + method: Schema.Literal(...HttpMethod.all), url: Schema.String, urlParams: Schema.Array(Schema.Tuple(Schema.String, Schema.String)), hash: Schema.Option(Schema.String), diff --git a/packages/ai/ai/test/AiError.test.ts b/packages/ai/ai/test/AiError.test.ts new file mode 100644 index 00000000000..9221de03b23 --- /dev/null +++ b/packages/ai/ai/test/AiError.test.ts @@ -0,0 +1,18 @@ +import * as AiError from "@effect/ai/AiError" +import { describe, it } from "@effect/vitest" +import { strictEqual } from "@effect/vitest/utils" +import { Schema } from "effect" + +describe("AiError", () => { + it("accepts uncommon HTTP methods in request details", () => { + const details = Schema.decodeUnknownSync(AiError.HttpRequestDetails)({ + method: "PROPFIND", + url: "https://example.com", + urlParams: [], + hash: { _tag: "None" }, + headers: {} + }) + + strictEqual(details.method, "PROPFIND") + }) +}) diff --git a/packages/platform/src/HttpLayerRouter.ts b/packages/platform/src/HttpLayerRouter.ts index dd467adc0ac..079c4ac49cd 100644 --- a/packages/platform/src/HttpLayerRouter.ts +++ b/packages/platform/src/HttpLayerRouter.ts @@ -58,7 +58,7 @@ export interface HttpRouter { readonly prefixed: (prefix: string) => HttpRouter readonly add: ( - method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS", + method: HttpMethod.HttpMethod | "*", path: PathInput, handler: | Effect.Effect @@ -299,7 +299,7 @@ export const use = ( * @category HttpRouter */ export const add = ( - method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS", + method: HttpMethod.HttpMethod | "*", path: PathInput, handler: | Effect.Effect @@ -442,7 +442,7 @@ const makeRoute = (options: { * @category Route */ export const route = ( - method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS", + method: HttpMethod.HttpMethod | "*", path: PathInput, handler: | Effect.Effect diff --git a/packages/platform/src/HttpMethod.ts b/packages/platform/src/HttpMethod.ts index e071ece924d..0e6faf3a92d 100644 --- a/packages/platform/src/HttpMethod.ts +++ b/packages/platform/src/HttpMethod.ts @@ -10,6 +10,17 @@ export type HttpMethod = | "PATCH" | "HEAD" | "OPTIONS" + | "PROPFIND" + | "PROPPATCH" + | "MKCOL" + | "COPY" + | "MOVE" + | "LOCK" + | "UNLOCK" + | "TRACE" + | "SEARCH" + | "REPORT" + | "MKCALENDAR" /** * @since 1.0.0 @@ -20,7 +31,7 @@ export declare namespace HttpMethod { * @since 1.0.0 * @category models */ - export type NoBody = "GET" | "HEAD" | "OPTIONS" + export type NoBody = "GET" | "HEAD" | "OPTIONS" | "TRACE" /** * @since 1.0.0 @@ -32,12 +43,32 @@ export declare namespace HttpMethod { /** * @since 1.0.0 */ -export const hasBody = (method: HttpMethod): boolean => method !== "GET" && method !== "HEAD" && method !== "OPTIONS" +export const hasBody = (method: HttpMethod): boolean => + method !== "GET" && method !== "HEAD" && method !== "OPTIONS" && method !== "TRACE" /** * @since 1.0.0 */ -export const all: ReadonlySet = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) +export const all: ReadonlySet = new Set([ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS", + "PROPFIND", + "PROPPATCH", + "MKCOL", + "COPY", + "MOVE", + "LOCK", + "UNLOCK", + "TRACE", + "SEARCH", + "REPORT", + "MKCALENDAR" +]) /** * Tests if a value is a `HttpMethod`. diff --git a/packages/platform/test/HttpMethod.test.ts b/packages/platform/test/HttpMethod.test.ts new file mode 100644 index 00000000000..ab3d0eb42bf --- /dev/null +++ b/packages/platform/test/HttpMethod.test.ts @@ -0,0 +1,39 @@ +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import * as HttpMethod from "@effect/platform/HttpMethod" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import { describe, it } from "@effect/vitest" +import { assertFalse, assertTrue, strictEqual } from "@effect/vitest/utils" + +const uncommonMethods = [ + "PROPFIND", + "PROPPATCH", + "MKCOL", + "COPY", + "MOVE", + "LOCK", + "UNLOCK", + "TRACE", + "SEARCH", + "REPORT", + "MKCALENDAR" +] as const satisfies ReadonlyArray + +describe("HttpMethod", () => { + it("recognizes uncommon HTTP methods", () => { + for (const method of uncommonMethods) { + assertTrue(HttpMethod.isHttpMethod(method)) + assertTrue(HttpMethod.all.has(method)) + } + }) + + it("treats TRACE as a no-body method", () => { + assertFalse(HttpMethod.hasBody("TRACE")) + }) + + it("supports uncommon methods in HttpLayerRouter", () => { + for (const method of uncommonMethods) { + const route = HttpLayerRouter.route(method, "/", HttpServerResponse.empty()) + strictEqual(route.method, method) + } + }) +})