Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: fix
packages:
- "@typespec/http"
---

- 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.
6 changes: 6 additions & 0 deletions packages/http/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ export const $lib = createTypeSpecLibrary({
default: "`Content-Type` header ignored because there is no body.",
},
},
"head-operation-no-body": {
severity: "warning",
messages: {
default: "head request must not return a message-body in the response",
},
},
"metadata-ignored": {
severity: "warning",
messages: {
Expand Down
18 changes: 14 additions & 4 deletions packages/http/src/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
getStatusCodesWithDiagnostics,
} from "./decorators.js";
import { HttpProperty } from "./http-property.js";
import { HttpStateKeys, 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";
Expand Down Expand Up @@ -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)) {
Expand All @@ -151,6 +148,19 @@ 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-operation-no-body",
target: responseType,
}),
);
}

// Get response headers
const headers = getResponseHeaders(program, metadata);

// Put them into currentEndpoint.responses
for (const statusCode of statusCodes) {
// the first model for this statusCode/content type pair carries the
Expand Down
46 changes: 46 additions & 0 deletions packages/http/test/responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,52 @@ 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-operation-no-body",
severity: "warning",
},
]);
strictEqual(routes.length, 1);
const response = routes[0].responses[0].responses[0];
ok(response.body);
// content-type is in the response headers (treated as a header for HEAD verb)
ok(response.headers["content-type"]);
});

it("emits a warning when HEAD @error 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-operation-no-body",
severity: "warning",
},
]);
strictEqual(routes.length, 1);
});

// 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(
Expand Down
12 changes: 11 additions & 1 deletion packages/http/test/verbs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,21 @@ 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");
});

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" }]);
});
});

describe("emit error when using 2 verb decorator together on the same node", () => {
Expand Down
16 changes: 0 additions & 16 deletions packages/openapi3/test/tsp-openapi3/paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,6 @@ model Foo {
responses: {
[statusCode]: {
description: "Test Response",
content: { "application/json": { schema: { $ref: "#/components/schemas/Foo" } } },
} as OpenAPI3Response,
},
},
Expand Down Expand Up @@ -978,7 +977,6 @@ model Foo {

@route("/") @head op headFoo(): {
@statusCode statusCode: 100;
@body body: Foo;
};
"
`);
Expand Down Expand Up @@ -1155,15 +1153,6 @@ model Foo {
},
},
},
head: {
operationId: "headFoo",
parameters: [],
responses: {
default: {
$ref: "#/components/responses/TestResponse",
},
},
},
},
},
});
Expand All @@ -1190,11 +1179,6 @@ model Foo {
Body = Foo
>;

@route("/") @head op headFoo(): GeneratedHelpers.DefaultResponse<
Description = "Base description",
Body = Foo
>;

namespace GeneratedHelpers {
@doc(Description)
@error
Expand Down
Loading