From 0737d66d6beff7e6b5438bb711139e43b77d00ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:26:02 +0000 Subject: [PATCH 1/8] Initial plan From d4a3b1f30157d3af2aa85b89ca339649fdc422ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:50:26 +0000 Subject: [PATCH 2/8] fix: @head request with body wrongly flags content type - emit head-verb-body warning Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...equest-content-type-2026-03-02-19-45-00.md | 7 +++ packages/http/src/lib.ts | 7 +++ packages/http/src/payload.ts | 14 +++-- packages/http/src/responses.ts | 29 ++++++++-- packages/http/test/responses.test.ts | 53 +++++++++++++++++++ packages/http/test/verbs.test.ts | 7 ++- .../openapi3/test/tsp-openapi3/paths.test.ts | 4 +- .../tsp-openapi3/utils/tsp-for-openapi3.ts | 12 ++++- 8 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 .chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md diff --git a/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md b/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md new file mode 100644 index 00000000000..f813907c376 --- /dev/null +++ b/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http" +--- + +Emit a `head-verb-body` warning when a `@head` operation response contains a body (which is against the HTTP spec). Additionally, fix the incorrect content-type handling that previously caused body content-types to be moved to headers when using `@head` verb with response models that contain both a content-type header and body properties. diff --git a/packages/http/src/lib.ts b/packages/http/src/lib.ts index 7cb09b523bc..02f2903e4bc 100644 --- a/packages/http/src/lib.ts +++ b/packages/http/src/lib.ts @@ -96,6 +96,13 @@ export const $lib = createTypeSpecLibrary({ default: "`Content-Type` header ignored because there is no body.", }, }, + "head-verb-body": { + severity: "warning", + messages: { + default: + "Operation with `@head` verb should not have a response body. The HTTP specification does not allow response bodies for HEAD requests.", + }, + }, "metadata-ignored": { severity: "warning", messages: { diff --git a/packages/http/src/payload.ts b/packages/http/src/payload.ts index d69d68b57a2..02bb184c8f2 100644 --- a/packages/http/src/payload.ts +++ b/packages/http/src/payload.ts @@ -45,7 +45,14 @@ export interface HttpPayload { readonly metadata: HttpProperty[]; } -export interface ExtractBodyAndMetadataOptions extends GetHttpPropertyOptions {} +export interface ExtractBodyAndMetadataOptions extends GetHttpPropertyOptions { + /** + * When true, suppress the `content-type-ignored` diagnostic when there is no body but + * a `Content-Type` property is present. Used for HEAD responses where content-type + * is a valid response header even when there is no body. + */ + allowContentTypeWithoutBody?: boolean; +} /** * The disposition of a payload in an HTTP operation. @@ -78,7 +85,7 @@ export function resolveHttpPayload( resolvePayloadProperties(program, type, visibility, disposition, options), ); - const body = diagnostics.pipe(resolveBody(program, type, metadata, visibility, disposition)); + const body = diagnostics.pipe(resolveBody(program, type, metadata, visibility, disposition, options)); if (body) { if ( @@ -104,6 +111,7 @@ function resolveBody( metadata: HttpProperty[], visibility: Visibility, disposition: HttpPayloadDisposition, + options: ExtractBodyAndMetadataOptions = {}, ): DiagnosticResult { const diagnostics = createDiagnosticCollector(); @@ -207,7 +215,7 @@ function resolveBody( ); } } - if (resolvedBody === undefined && contentTypeProperty) { + if (resolvedBody === undefined && contentTypeProperty && !options.allowContentTypeWithoutBody) { diagnostics.add( createDiagnostic({ code: "content-type-ignored", diff --git a/packages/http/src/responses.ts b/packages/http/src/responses.ts index 281030fd339..7022b555920 100644 --- a/packages/http/src/responses.ts +++ b/packages/http/src/responses.ts @@ -21,7 +21,7 @@ import { getStatusCodesWithDiagnostics, } from "./decorators.js"; import { HttpProperty } from "./http-property.js"; -import { HttpStateKeys, reportDiagnostic } from "./lib.js"; +import { HttpStateKeys, createDiagnostic, reportDiagnostic } from "./lib.js"; import { Visibility } from "./metadata.js"; import { HttpPayloadDisposition, resolveHttpPayload } from "./payload.js"; import { HttpOperationResponse, HttpStatusCodes, HttpStatusCodesEntry } from "./types.js"; @@ -125,7 +125,7 @@ function processResponseType( const verb = getOperationVerb(program, operation); let { body: resolvedBody, metadata } = diagnostics.pipe( resolveHttpPayload(program, responseType, Visibility.Read, HttpPayloadDisposition.Response, { - treatContentTypeAsHeader: verb === "head", + allowContentTypeWithoutBody: verb === "head", }), ); // Get explicity defined status codes @@ -133,9 +133,6 @@ function processResponseType( getResponseStatusCodes(program, responseType, metadata), ); - // Get response headers - const headers = getResponseHeaders(program, metadata); - // If there is no explicit status code, check if it should be 204 if (statusCodes.length === 0) { if (isErrorModel(program, responseType)) { @@ -151,6 +148,28 @@ function processResponseType( } } + // Emit a warning if a HEAD response has a body (HTTP spec disallows this) + if (verb === "head" && resolvedBody !== undefined) { + diagnostics.add( + createDiagnostic({ + code: "head-verb-body", + target: responseType, + }), + ); + } + + // Get response headers + const headers = getResponseHeaders(program, metadata); + + // For HEAD responses with no body, include the content-type as a response header + // (HTTP spec allows HEAD to return the same headers as GET, even though the body is omitted) + if (verb === "head" && resolvedBody === undefined) { + const contentTypeProperty = metadata.find((x) => x.kind === "contentType"); + if (contentTypeProperty) { + headers["content-type"] = contentTypeProperty.property; + } + } + // Put them into currentEndpoint.responses for (const statusCode of statusCodes) { // the first model for this statusCode/content type pair carries the diff --git a/packages/http/test/responses.test.ts b/packages/http/test/responses.test.ts index 98a40b72185..15fb0d6c802 100644 --- a/packages/http/test/responses.test.ts +++ b/packages/http/test/responses.test.ts @@ -156,6 +156,59 @@ it("treats content-type as a header for HEAD responses", async () => { deepStrictEqual(Object.keys(response.headers), ["content-type"]); }); +it("emits a warning when HEAD response has a body", async () => { + const [routes, diagnostics] = await getOperationsWithServiceNamespace( + ` + @head + op head(): { @header contentType: "text/plain"; value: string }; + `, + ); + + expectDiagnostics(diagnostics, [ + { + code: "@typespec/http/head-verb-body", + severity: "warning", + }, + ]); + strictEqual(routes.length, 1); + const response = routes[0].responses[0].responses[0]; + // Body should still be present and use the correct content-type + ok(response.body); + deepStrictEqual(response.body.contentTypes, ["text/plain"]); +}); + +it("respects content-type from model when HEAD response has a body", async () => { + const [routes, diagnostics] = await getOperationsWithServiceNamespace( + ` + @head + op listHead(): OkResponse | Error; + + @error + model Error { + @header + contentType: "application/problem+json"; + type: string; + } + `, + ); + + // Should get warning about body in HEAD response + expectDiagnostics(diagnostics, [ + { + code: "@typespec/http/head-verb-body", + severity: "warning", + }, + ]); + strictEqual(routes.length, 1); + // Find the error response + const errorResponse = routes[0].responses.find((r) => r.statusCodes === "*"); + ok(errorResponse); + const responseContent = errorResponse.responses[0]; + // Body should use the correct content-type from the model + ok(responseContent.body); + deepStrictEqual(responseContent.body.contentTypes, ["application/problem+json"]); +}); + // Regression test for https://github.com/microsoft/typespec/issues/328 it("empty response model becomes body if it has children", async () => { const [routes, diagnostics] = await getOperationsWithServiceNamespace( diff --git a/packages/http/test/verbs.test.ts b/packages/http/test/verbs.test.ts index 48eb235288d..5b3d9846825 100644 --- a/packages/http/test/verbs.test.ts +++ b/packages/http/test/verbs.test.ts @@ -9,11 +9,16 @@ describe("specify verb with each decorator", () => { ["@put", "put"], ["@patch", "patch"], ["@delete", "delete"], - ["@head", "head"], ])("%s set verb to %s", async (dec, expected) => { const routes = await getRoutesFor(`${dec} op test(): string;`); expect(routes[0].verb).toBe(expected); }); + + it("@head set verb to head", async () => { + // Use void to avoid triggering the head-verb-body warning + const routes = await getRoutesFor(`@head op test(): void;`); + expect(routes[0].verb).toBe("head"); + }); }); describe("emit error when using 2 verb decorator together on the same node", () => { diff --git a/packages/openapi3/test/tsp-openapi3/paths.test.ts b/packages/openapi3/test/tsp-openapi3/paths.test.ts index 26802c7ad6d..93635c4d025 100644 --- a/packages/openapi3/test/tsp-openapi3/paths.test.ts +++ b/packages/openapi3/test/tsp-openapi3/paths.test.ts @@ -983,7 +983,7 @@ model Foo { " `); - await validateTsp(tsp); + await validateTsp(tsp, [{ code: "@typespec/http/head-verb-body", severity: "warning" }]); }); }); @@ -1210,7 +1210,7 @@ model Foo { " `); - await validateTsp(tsp); + await validateTsp(tsp, [{ code: "@typespec/http/head-verb-body", severity: "warning" }]); }); it("supports header references", async () => { diff --git a/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts b/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts index 455aa01f71f..6a26fc5d134 100644 --- a/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts +++ b/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts @@ -2,6 +2,7 @@ import { Diagnostic, Namespace, Program } from "@typespec/compiler"; import { createTestHost as coreCreateTestHost, expectDiagnosticEmpty, + expectDiagnostics, } from "@typespec/compiler/testing"; import { HttpTestLibrary } from "@typespec/http/testing"; import { OpenAPITestLibrary } from "@typespec/openapi/testing"; @@ -38,11 +39,18 @@ async function createTestHost() { }); } -export async function validateTsp(code: string) { +export async function validateTsp( + code: string, + expectedDiagnostics?: readonly { code: string; severity?: string }[], +) { const host = await createTestHost(); host.addTypeSpecFile("main.tsp", code); const [, diagnostics] = await host.compileAndDiagnose("main.tsp"); - expectDiagnosticEmpty(diagnostics); + if (expectedDiagnostics && expectedDiagnostics.length > 0) { + expectDiagnostics(diagnostics, expectedDiagnostics as any); + } else { + expectDiagnosticEmpty(diagnostics); + } } export async function tspForOpenAPI3(props: OpenAPI3Options) { From 40877dc67c08f610e64974c93a4f51904ab34eee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:57:19 +0000 Subject: [PATCH 3/8] fix: address code review - add head-verb-body warning test in verbs.test.ts, improve changeset Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...ilot-fix-head-request-content-type-2026-03-02-19-45-00.md | 3 ++- packages/http/src/payload.ts | 4 +++- packages/http/src/responses.ts | 2 +- packages/http/test/verbs.test.ts | 5 +++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md b/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md index f813907c376..f7973b665ff 100644 --- a/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md +++ b/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md @@ -4,4 +4,5 @@ packages: - "@typespec/http" --- -Emit a `head-verb-body` warning when a `@head` operation response contains a body (which is against the HTTP spec). Additionally, fix the incorrect content-type handling that previously caused body content-types to be moved to headers when using `@head` verb with response models that contain both a content-type header and body properties. +- Emit a `head-verb-body` warning when a `@head` operation response contains a body (which is against the HTTP specification). +- Fix incorrect content-type handling where body content-types were wrongly moved to headers when `@head` verb was used with response models containing both a content-type header property and body properties. diff --git a/packages/http/src/payload.ts b/packages/http/src/payload.ts index 02bb184c8f2..9da54a24653 100644 --- a/packages/http/src/payload.ts +++ b/packages/http/src/payload.ts @@ -85,7 +85,9 @@ export function resolveHttpPayload( resolvePayloadProperties(program, type, visibility, disposition, options), ); - const body = diagnostics.pipe(resolveBody(program, type, metadata, visibility, disposition, options)); + const body = diagnostics.pipe( + resolveBody(program, type, metadata, visibility, disposition, options), + ); if (body) { if ( diff --git a/packages/http/src/responses.ts b/packages/http/src/responses.ts index 7022b555920..aec8ce75c16 100644 --- a/packages/http/src/responses.ts +++ b/packages/http/src/responses.ts @@ -21,7 +21,7 @@ import { getStatusCodesWithDiagnostics, } from "./decorators.js"; import { HttpProperty } from "./http-property.js"; -import { HttpStateKeys, createDiagnostic, reportDiagnostic } from "./lib.js"; +import { createDiagnostic, HttpStateKeys, reportDiagnostic } from "./lib.js"; import { Visibility } from "./metadata.js"; import { HttpPayloadDisposition, resolveHttpPayload } from "./payload.js"; import { HttpOperationResponse, HttpStatusCodes, HttpStatusCodesEntry } from "./types.js"; diff --git a/packages/http/test/verbs.test.ts b/packages/http/test/verbs.test.ts index 5b3d9846825..6c2fa5ce9b5 100644 --- a/packages/http/test/verbs.test.ts +++ b/packages/http/test/verbs.test.ts @@ -19,6 +19,11 @@ describe("specify verb with each decorator", () => { const routes = await getRoutesFor(`@head op test(): void;`); expect(routes[0].verb).toBe("head"); }); + + it("@head with body emits head-verb-body warning", async () => { + const diagnostics = await diagnoseOperations(`@head op test(): string;`); + expectDiagnostics(diagnostics, [{ code: "@typespec/http/head-verb-body", severity: "warning" }]); + }); }); describe("emit error when using 2 verb decorator together on the same node", () => { From a24fe2f8d220a10bfb2257f684b5a48ae96decde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:34:40 +0000 Subject: [PATCH 4/8] fix: address reviewer feedback - rename to head-no-body, simplify to treatContentTypeAsHeader, revert tsp-for-openapi3.ts Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...equest-content-type-2026-03-02-19-45-00.md | 4 ++-- packages/http/src/lib.ts | 5 ++--- packages/http/src/payload.ts | 16 +++------------ packages/http/src/responses.ts | 13 ++---------- packages/http/test/responses.test.ts | 17 +++++----------- packages/http/test/verbs.test.ts | 4 ++-- .../openapi3/test/tsp-openapi3/paths.test.ts | 20 ++----------------- .../tsp-openapi3/utils/tsp-for-openapi3.ts | 12 ++--------- 8 files changed, 20 insertions(+), 71 deletions(-) diff --git a/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md b/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md index f7973b665ff..20f85944838 100644 --- a/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md +++ b/.chronus/changes/copilot-fix-head-request-content-type-2026-03-02-19-45-00.md @@ -4,5 +4,5 @@ packages: - "@typespec/http" --- -- Emit a `head-verb-body` warning when a `@head` operation response contains a body (which is against the HTTP specification). -- Fix incorrect content-type handling where body content-types were wrongly moved to headers when `@head` verb was used with response models containing both a content-type header property and body properties. +- Emit a `head-no-body` warning when a `@head` operation response contains a body (HTTP spec: "head request must not return a message-body in the response"). +- Fix the `content-type-ignored` warning incorrectly firing for `@head` responses that have a content-type header but no body. diff --git a/packages/http/src/lib.ts b/packages/http/src/lib.ts index 02f2903e4bc..842fda845ce 100644 --- a/packages/http/src/lib.ts +++ b/packages/http/src/lib.ts @@ -96,11 +96,10 @@ export const $lib = createTypeSpecLibrary({ default: "`Content-Type` header ignored because there is no body.", }, }, - "head-verb-body": { + "head-no-body": { severity: "warning", messages: { - default: - "Operation with `@head` verb should not have a response body. The HTTP specification does not allow response bodies for HEAD requests.", + default: "head request must not return a message-body in the response", }, }, "metadata-ignored": { diff --git a/packages/http/src/payload.ts b/packages/http/src/payload.ts index 9da54a24653..d69d68b57a2 100644 --- a/packages/http/src/payload.ts +++ b/packages/http/src/payload.ts @@ -45,14 +45,7 @@ export interface HttpPayload { readonly metadata: HttpProperty[]; } -export interface ExtractBodyAndMetadataOptions extends GetHttpPropertyOptions { - /** - * When true, suppress the `content-type-ignored` diagnostic when there is no body but - * a `Content-Type` property is present. Used for HEAD responses where content-type - * is a valid response header even when there is no body. - */ - allowContentTypeWithoutBody?: boolean; -} +export interface ExtractBodyAndMetadataOptions extends GetHttpPropertyOptions {} /** * The disposition of a payload in an HTTP operation. @@ -85,9 +78,7 @@ export function resolveHttpPayload( resolvePayloadProperties(program, type, visibility, disposition, options), ); - const body = diagnostics.pipe( - resolveBody(program, type, metadata, visibility, disposition, options), - ); + const body = diagnostics.pipe(resolveBody(program, type, metadata, visibility, disposition)); if (body) { if ( @@ -113,7 +104,6 @@ function resolveBody( metadata: HttpProperty[], visibility: Visibility, disposition: HttpPayloadDisposition, - options: ExtractBodyAndMetadataOptions = {}, ): DiagnosticResult { const diagnostics = createDiagnosticCollector(); @@ -217,7 +207,7 @@ function resolveBody( ); } } - if (resolvedBody === undefined && contentTypeProperty && !options.allowContentTypeWithoutBody) { + if (resolvedBody === undefined && contentTypeProperty) { diagnostics.add( createDiagnostic({ code: "content-type-ignored", diff --git a/packages/http/src/responses.ts b/packages/http/src/responses.ts index aec8ce75c16..079cbfa5306 100644 --- a/packages/http/src/responses.ts +++ b/packages/http/src/responses.ts @@ -125,7 +125,7 @@ function processResponseType( const verb = getOperationVerb(program, operation); let { body: resolvedBody, metadata } = diagnostics.pipe( resolveHttpPayload(program, responseType, Visibility.Read, HttpPayloadDisposition.Response, { - allowContentTypeWithoutBody: verb === "head", + treatContentTypeAsHeader: verb === "head", }), ); // Get explicity defined status codes @@ -152,7 +152,7 @@ function processResponseType( if (verb === "head" && resolvedBody !== undefined) { diagnostics.add( createDiagnostic({ - code: "head-verb-body", + code: "head-no-body", target: responseType, }), ); @@ -161,15 +161,6 @@ function processResponseType( // Get response headers const headers = getResponseHeaders(program, metadata); - // For HEAD responses with no body, include the content-type as a response header - // (HTTP spec allows HEAD to return the same headers as GET, even though the body is omitted) - if (verb === "head" && resolvedBody === undefined) { - const contentTypeProperty = metadata.find((x) => x.kind === "contentType"); - if (contentTypeProperty) { - headers["content-type"] = contentTypeProperty.property; - } - } - // Put them into currentEndpoint.responses for (const statusCode of statusCodes) { // the first model for this statusCode/content type pair carries the diff --git a/packages/http/test/responses.test.ts b/packages/http/test/responses.test.ts index 15fb0d6c802..21e1335f659 100644 --- a/packages/http/test/responses.test.ts +++ b/packages/http/test/responses.test.ts @@ -166,18 +166,18 @@ it("emits a warning when HEAD response has a body", async () => { expectDiagnostics(diagnostics, [ { - code: "@typespec/http/head-verb-body", + code: "@typespec/http/head-no-body", severity: "warning", }, ]); strictEqual(routes.length, 1); const response = routes[0].responses[0].responses[0]; - // Body should still be present and use the correct content-type ok(response.body); - deepStrictEqual(response.body.contentTypes, ["text/plain"]); + // content-type is in the response headers (treated as a header for HEAD verb) + ok(response.headers["content-type"]); }); -it("respects content-type from model when HEAD response has a body", async () => { +it("emits a warning when HEAD @error response has a body", async () => { const [routes, diagnostics] = await getOperationsWithServiceNamespace( ` @head @@ -195,18 +195,11 @@ it("respects content-type from model when HEAD response has a body", async () => // Should get warning about body in HEAD response expectDiagnostics(diagnostics, [ { - code: "@typespec/http/head-verb-body", + code: "@typespec/http/head-no-body", severity: "warning", }, ]); strictEqual(routes.length, 1); - // Find the error response - const errorResponse = routes[0].responses.find((r) => r.statusCodes === "*"); - ok(errorResponse); - const responseContent = errorResponse.responses[0]; - // Body should use the correct content-type from the model - ok(responseContent.body); - deepStrictEqual(responseContent.body.contentTypes, ["application/problem+json"]); }); // Regression test for https://github.com/microsoft/typespec/issues/328 diff --git a/packages/http/test/verbs.test.ts b/packages/http/test/verbs.test.ts index 6c2fa5ce9b5..c0dc72cfb2d 100644 --- a/packages/http/test/verbs.test.ts +++ b/packages/http/test/verbs.test.ts @@ -20,9 +20,9 @@ describe("specify verb with each decorator", () => { expect(routes[0].verb).toBe("head"); }); - it("@head with body emits head-verb-body warning", async () => { + it("@head with body emits head-no-body warning", async () => { const diagnostics = await diagnoseOperations(`@head op test(): string;`); - expectDiagnostics(diagnostics, [{ code: "@typespec/http/head-verb-body", severity: "warning" }]); + expectDiagnostics(diagnostics, [{ code: "@typespec/http/head-no-body", severity: "warning" }]); }); }); diff --git a/packages/openapi3/test/tsp-openapi3/paths.test.ts b/packages/openapi3/test/tsp-openapi3/paths.test.ts index 93635c4d025..c0bb496b28d 100644 --- a/packages/openapi3/test/tsp-openapi3/paths.test.ts +++ b/packages/openapi3/test/tsp-openapi3/paths.test.ts @@ -948,7 +948,6 @@ model Foo { responses: { [statusCode]: { description: "Test Response", - content: { "application/json": { schema: { $ref: "#/components/schemas/Foo" } } }, } as OpenAPI3Response, }, }, @@ -978,12 +977,11 @@ model Foo { @route("/") @head op headFoo(): { @statusCode statusCode: 100; - @body body: Foo; }; " `); - await validateTsp(tsp, [{ code: "@typespec/http/head-verb-body", severity: "warning" }]); + await validateTsp(tsp); }); }); @@ -1155,15 +1153,6 @@ model Foo { }, }, }, - head: { - operationId: "headFoo", - parameters: [], - responses: { - default: { - $ref: "#/components/responses/TestResponse", - }, - }, - }, }, }, }); @@ -1190,11 +1179,6 @@ model Foo { Body = Foo >; - @route("/") @head op headFoo(): GeneratedHelpers.DefaultResponse< - Description = "Base description", - Body = Foo - >; - namespace GeneratedHelpers { @doc(Description) @error @@ -1210,7 +1194,7 @@ model Foo { " `); - await validateTsp(tsp, [{ code: "@typespec/http/head-verb-body", severity: "warning" }]); + await validateTsp(tsp); }); it("supports header references", async () => { diff --git a/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts b/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts index 6a26fc5d134..455aa01f71f 100644 --- a/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts +++ b/packages/openapi3/test/tsp-openapi3/utils/tsp-for-openapi3.ts @@ -2,7 +2,6 @@ import { Diagnostic, Namespace, Program } from "@typespec/compiler"; import { createTestHost as coreCreateTestHost, expectDiagnosticEmpty, - expectDiagnostics, } from "@typespec/compiler/testing"; import { HttpTestLibrary } from "@typespec/http/testing"; import { OpenAPITestLibrary } from "@typespec/openapi/testing"; @@ -39,18 +38,11 @@ async function createTestHost() { }); } -export async function validateTsp( - code: string, - expectedDiagnostics?: readonly { code: string; severity?: string }[], -) { +export async function validateTsp(code: string) { const host = await createTestHost(); host.addTypeSpecFile("main.tsp", code); const [, diagnostics] = await host.compileAndDiagnose("main.tsp"); - if (expectedDiagnostics && expectedDiagnostics.length > 0) { - expectDiagnostics(diagnostics, expectedDiagnostics as any); - } else { - expectDiagnosticEmpty(diagnostics); - } + expectDiagnosticEmpty(diagnostics); } export async function tspForOpenAPI3(props: OpenAPI3Options) { From c6c60f984361f01fc1421d314b316cf5ad48441f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:18:30 +0000 Subject: [PATCH 5/8] fix: rename diagnostic to head-operation-no-body Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http/src/lib.ts | 2 +- packages/http/src/responses.ts | 2 +- packages/http/test/responses.test.ts | 4 ++-- packages/http/test/verbs.test.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/http/src/lib.ts b/packages/http/src/lib.ts index 842fda845ce..5413aba2a6e 100644 --- a/packages/http/src/lib.ts +++ b/packages/http/src/lib.ts @@ -96,7 +96,7 @@ export const $lib = createTypeSpecLibrary({ default: "`Content-Type` header ignored because there is no body.", }, }, - "head-no-body": { + "head-operation-no-body": { severity: "warning", messages: { default: "head request must not return a message-body in the response", diff --git a/packages/http/src/responses.ts b/packages/http/src/responses.ts index 079cbfa5306..3c9f82abe26 100644 --- a/packages/http/src/responses.ts +++ b/packages/http/src/responses.ts @@ -152,7 +152,7 @@ function processResponseType( if (verb === "head" && resolvedBody !== undefined) { diagnostics.add( createDiagnostic({ - code: "head-no-body", + code: "head-operation-no-body", target: responseType, }), ); diff --git a/packages/http/test/responses.test.ts b/packages/http/test/responses.test.ts index 21e1335f659..2056787c497 100644 --- a/packages/http/test/responses.test.ts +++ b/packages/http/test/responses.test.ts @@ -166,7 +166,7 @@ it("emits a warning when HEAD response has a body", async () => { expectDiagnostics(diagnostics, [ { - code: "@typespec/http/head-no-body", + code: "@typespec/http/head-operation-no-body", severity: "warning", }, ]); @@ -195,7 +195,7 @@ it("emits a warning when HEAD @error response has a body", async () => { // Should get warning about body in HEAD response expectDiagnostics(diagnostics, [ { - code: "@typespec/http/head-no-body", + code: "@typespec/http/head-operation-no-body", severity: "warning", }, ]); diff --git a/packages/http/test/verbs.test.ts b/packages/http/test/verbs.test.ts index c0dc72cfb2d..aa9134d30bf 100644 --- a/packages/http/test/verbs.test.ts +++ b/packages/http/test/verbs.test.ts @@ -20,9 +20,9 @@ describe("specify verb with each decorator", () => { expect(routes[0].verb).toBe("head"); }); - it("@head with body emits head-no-body warning", async () => { + it("@head with body emits head-operation-no-body warning", async () => { const diagnostics = await diagnoseOperations(`@head op test(): string;`); - expectDiagnostics(diagnostics, [{ code: "@typespec/http/head-no-body", severity: "warning" }]); + expectDiagnostics(diagnostics, [{ code: "@typespec/http/head-operation-no-body", severity: "warning" }]); }); }); From 433f22ac37d090788820341507812e29ef52efea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:49:24 +0000 Subject: [PATCH 6/8] fix: format verbs.test.ts Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http/test/verbs.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/http/test/verbs.test.ts b/packages/http/test/verbs.test.ts index aa9134d30bf..30493fa66aa 100644 --- a/packages/http/test/verbs.test.ts +++ b/packages/http/test/verbs.test.ts @@ -22,7 +22,9 @@ describe("specify verb with each decorator", () => { it("@head with body emits head-operation-no-body warning", async () => { const diagnostics = await diagnoseOperations(`@head op test(): string;`); - expectDiagnostics(diagnostics, [{ code: "@typespec/http/head-operation-no-body", severity: "warning" }]); + expectDiagnostics(diagnostics, [ + { code: "@typespec/http/head-operation-no-body", severity: "warning" }, + ]); }); }); From d430ee827618a46c9a473e2b1e3d4bfe666144cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:02:10 +0000 Subject: [PATCH 7/8] fix: remove array wrapper from single expectDiagnostics call in verbs.test.ts Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http/test/verbs.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/http/test/verbs.test.ts b/packages/http/test/verbs.test.ts index 30493fa66aa..43e52b3ddae 100644 --- a/packages/http/test/verbs.test.ts +++ b/packages/http/test/verbs.test.ts @@ -22,9 +22,10 @@ describe("specify verb with each decorator", () => { it("@head with body emits head-operation-no-body warning", async () => { const diagnostics = await diagnoseOperations(`@head op test(): string;`); - expectDiagnostics(diagnostics, [ - { code: "@typespec/http/head-operation-no-body", severity: "warning" }, - ]); + expectDiagnostics(diagnostics, { + code: "@typespec/http/head-operation-no-body", + severity: "warning", + }); }); }); From 03b9e2b0d02728d76b46402a6b16eafc83e771fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:27:08 +0000 Subject: [PATCH 8/8] Changes before error encountered Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http/src/responses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http/src/responses.ts b/packages/http/src/responses.ts index 3c9f82abe26..da76d8b84e8 100644 --- a/packages/http/src/responses.ts +++ b/packages/http/src/responses.ts @@ -153,7 +153,7 @@ function processResponseType( diagnostics.add( createDiagnostic({ code: "head-operation-no-body", - target: responseType, + target: operation, }), ); }