Skip to content
Open
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,7 @@
---
changeKind: feature
packages:
- "@typespec/http-specs"
---

Add new test case for multipart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/http-client-js"
---

skip multipart test case until bug is fixed to unblock new spector case merge
4 changes: 4 additions & 0 deletions packages/http-client-js/.testignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ streaming
# discriminated union issue - https://github.com/microsoft/typespec/issues/7134

type/union/discriminated

# Multipart - https://github.com/microsoft/typespec/issues/9155

payload/multipart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,49 @@ import { readFile } from "fs/promises";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import { beforeEach, describe, it } from "vitest";
import { FormDataClient, HttpPartsClient } from "../../../generated/payload/multipart/src/index.js";
// import { FormDataClient, HttpPartsClient } from "../../../generated/payload/multipart/src/index.js";

// Temporary stubs to avoid build errors while generator bug is fixed
class FormDataClient {
constructor(_: { allowInsecureConnection?: boolean; retryOptions?: { maxRetries?: number } }) {}
async basic(_: { id: string; profileImage: Uint8Array }): Promise<void> {}
async fileArrayAndBasic(_: {
id: string;
address: unknown;
profileImage: Uint8Array;
pictures: Uint8Array[];
}): Promise<void> {}
async jsonPart(_: { address: unknown; profileImage: Uint8Array }): Promise<void> {}
async binaryArrayParts(_: { id: string; pictures: Uint8Array[] }): Promise<void> {}
async multiBinaryParts(_: { profileImage: Uint8Array; picture: Uint8Array }): Promise<void> {}
async checkFileNameAndContentType(_: { id: string; profileImage: Uint8Array }): Promise<void> {}
async anonymousModel(_: { profileImage: Uint8Array }): Promise<void> {}
}

class HttpPartsClient {
constructor(_: { allowInsecureConnection?: boolean; retryOptions?: { maxRetries?: number } }) {}
contentTypeClient = {
async imageJpegContentType(_: {
profileImage: { contents: Uint8Array; contentType: string; filename: string };
}): Promise<void> {},
async requiredContentType(_: {
profileImage: { contents: Uint8Array; contentType: string; filename: string };
}): Promise<void> {},
async optionalContentType(_: {
profileImage: { contents: Uint8Array; filename: string };
}): Promise<void> {},
};
async jsonArrayAndFileArray(_: {
id: string;
address: unknown;
profileImage: { contents: Uint8Array; contentType: string; filename: string };
previousAddresses: unknown[];
pictures: Array<{ contents: Uint8Array; contentType: string; filename: string }>;
}): Promise<void> {}
nonStringClient = {
async float(_: { temperature: { body: number; contentType: string } }): Promise<void> {},
};
}

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand All @@ -15,7 +57,7 @@ const pngImagePath = resolve(__dirname, "../../../assets/image.png");
const pngBuffer = await readFile(pngImagePath);
const pngContents = new Uint8Array(pngBuffer);

describe("Payload.MultiPart", () => {
describe.skip("Payload.MultiPart", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After #9155 fixed, js could revert the change.
CC @MaryGao

Copy link
Member

@MaryGao MaryGao Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joheredi the new mfd cases would fail the generation so here we skip the whold mfd cases. issue is tracked here: #9155.

// Skipping as implicit multipart is going to be deprecated in TypeSpec
describe.skip("FormDataClient", () => {
const client = new FormDataClient({
Expand Down Expand Up @@ -78,7 +120,7 @@ describe("Payload.MultiPart", () => {
});
});

describe("FormDataClient.HttpParts.ContentType", () => {
describe.skip("FormDataClient.HttpParts.ContentType", () => {
const client = new HttpPartsClient({
allowInsecureConnection: true,
retryOptions: { maxRetries: 1 },
Expand Down Expand Up @@ -114,7 +156,7 @@ describe("Payload.MultiPart", () => {
});
});

describe("FormDataClient.HttpParts", () => {
describe.skip("FormDataClient.HttpParts", () => {
it("should send json array and file array", async () => {
const client = new HttpPartsClient({
allowInsecureConnection: true,
Expand All @@ -141,7 +183,7 @@ describe("Payload.MultiPart", () => {
});
});

describe("FormDataClient.HttpParts.NonString", () => {
describe.skip("FormDataClient.HttpParts.NonString", () => {
it("should handle non-string float", async () => {
const client = new HttpPartsClient({
allowInsecureConnection: true,
Expand Down
92 changes: 92 additions & 0 deletions packages/http-specs/spec-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -2109,6 +2109,98 @@ Content-Type: application/octet-stream
--abcde12345--
```

### Payload_MultiPart_FormData_optionalParts

- Endpoint: `post /multipart/form-data/optional-parts`

Please send request three times:

- First time with only id
- Second time with only profileImage
- Third time with both id and profileImage

Expect requests (

- according to https://datatracker.ietf.org/doc/html/rfc7578#section-4.4, content-type of file part shall be labeled with
appropriate media type, server will check it; content-type of other parts is optional, server will ignore it.
- according to https://datatracker.ietf.org/doc/html/rfc7578#section-4.2, filename of file part SHOULD be supplied.
If there are duplicated filename in same fieldName, server can't parse them all.
):

```
POST /upload HTTP/1.1
Content-Length: 428
Content-Type: multipart/form-data; boundary=abcde12345

--abcde12345
Content-Disposition: form-data; name="id"
Content-Type: text/plain

123
--abcde12345--
```

```
POST /upload HTTP/1.1
Content-Length: 428
Content-Type: multipart/form-data; boundary=abcde12345

--abcde12345
Content-Disposition: form-data; name="profileImage"; filename="<any-or-no-name-is-ok>"
Content-Type: application/octet-stream

{…file content of .jpg file…}
--abcde12345--
```

```
POST /upload HTTP/1.1
Content-Length: 428
Content-Type: multipart/form-data; boundary=abcde12345

--abcde12345
Content-Disposition: form-data; name="id"
Content-Type: text/plain

123
--abcde12345
Content-Disposition: form-data; name="profileImage"; filename="<any-or-no-name-is-ok>"
Content-Type: application/octet-stream

{…file content of .jpg file…}
--abcde12345--
```

### Payload_MultiPart_FormData_withWireName

- Endpoint: `post /multipart/form-data/mixed-parts-with-wire-name`

Expect request with wire names (

- according to https://datatracker.ietf.org/doc/html/rfc7578#section-4.4, content-type of file part shall be labeled with
appropriate media type, server will check it; content-type of other parts is optional, server will ignore it.
- according to https://datatracker.ietf.org/doc/html/rfc7578#section-4.2, filename of file part SHOULD be supplied.
If there are duplicated filename in same fieldName, server can't parse them all.
):

```
POST /upload HTTP/1.1
Content-Length: 428
Content-Type: multipart/form-data; boundary=abcde12345

--abcde12345
Content-Disposition: form-data; name="id"
Content-Type: text/plain

123
--abcde12345
Content-Disposition: form-data; name="profileImage"; filename="<any-or-no-name-is-ok>"
Content-Type: application/octet-stream;

{…file content of .jpg file…}
--abcde12345--
```

### Payload_Pageable_PageSize_listWithoutContinuation

- Endpoint: `get /payload/pageable/pagesize/without-continuation`
Expand Down
109 changes: 109 additions & 0 deletions packages/http-specs/specs/payload/multipart/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ model MultiPartRequest {
profileImage: HttpPart<bytes>;
}

model MultiPartRequestWithWireName {
identifier: HttpPart<string, #{ name: "id" }>;
image: HttpPart<bytes, #{ name: "profileImage" }>;
}

model MultiPartOptionalRequest {
id?: HttpPart<string>;
profileImage?: HttpPart<bytes>;
}

model Address {
city: string;
}
Expand Down Expand Up @@ -109,6 +119,105 @@ namespace FormData {
@multipartBody body: MultiPartRequest,
): NoContentResponse;

@scenario
@scenarioDoc("""
Expect request with wire names (
- according to https://datatracker.ietf.org/doc/html/rfc7578#section-4.4, content-type of file part shall be labeled with
appropriate media type, server will check it; content-type of other parts is optional, server will ignore it.
- according to https://datatracker.ietf.org/doc/html/rfc7578#section-4.2, filename of file part SHOULD be supplied.
If there are duplicated filename in same fieldName, server can't parse them all.
):
```
POST /upload HTTP/1.1
Content-Length: 428
Content-Type: multipart/form-data; boundary=abcde12345

--abcde12345
Content-Disposition: form-data; name="id"
Content-Type: text/plain

123
--abcde12345
Content-Disposition: form-data; name="profileImage"; filename="<any-or-no-name-is-ok>"
Content-Type: application/octet-stream;

{…file content of .jpg file…}
--abcde12345--
```
""")
@doc("Test content-type: multipart/form-data with wire names")
@post
@route("/mixed-parts-with-wire-name")
op withWireName(
@header contentType: "multipart/form-data",
@multipartBody body: MultiPartRequestWithWireName,
): NoContentResponse;

@scenario
@scenarioDoc("""
Please send request three times:
- First time with only id
- Second time with only profileImage
- Third time with both id and profileImage

Expect requests (
- according to https://datatracker.ietf.org/doc/html/rfc7578#section-4.4, content-type of file part shall be labeled with
appropriate media type, server will check it; content-type of other parts is optional, server will ignore it.
- according to https://datatracker.ietf.org/doc/html/rfc7578#section-4.2, filename of file part SHOULD be supplied.
If there are duplicated filename in same fieldName, server can't parse them all.
):
```
POST /upload HTTP/1.1
Content-Length: 428
Content-Type: multipart/form-data; boundary=abcde12345

--abcde12345
Content-Disposition: form-data; name="id"
Content-Type: text/plain

123
--abcde12345--
```

```
POST /upload HTTP/1.1
Content-Length: 428
Content-Type: multipart/form-data; boundary=abcde12345

--abcde12345
Content-Disposition: form-data; name="profileImage"; filename="<any-or-no-name-is-ok>"
Content-Type: application/octet-stream

{…file content of .jpg file…}
--abcde12345--
```

```
POST /upload HTTP/1.1
Content-Length: 428
Content-Type: multipart/form-data; boundary=abcde12345

--abcde12345
Content-Disposition: form-data; name="id"
Content-Type: text/plain

123
--abcde12345
Content-Disposition: form-data; name="profileImage"; filename="<any-or-no-name-is-ok>"
Content-Type: application/octet-stream

{…file content of .jpg file…}
--abcde12345--
```
""")
@doc("Test content-type: multipart/form-data with optional parts")
@post
@route("/optional-parts")
op optionalParts(
@header contentType: "multipart/form-data",
@multipartBody body: MultiPartOptionalRequest,
): NoContentResponse;

@scenario
@scenarioDoc("""
Expect request (
Expand Down
Loading
Loading