Skip to content

Commit c1ad9c9

Browse files
authored
Merge pull request #1471 from polywrap/http-plugin-formdata
http plugin form-data support
2 parents 2b39095 + 9730e3f commit c1ad9c9

5 files changed

Lines changed: 162 additions & 9 deletions

File tree

packages/interfaces/http/src/schema.graphql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,28 @@ type Request {
99
headers: Map @annotate(type: "Map<String!, String!>")
1010
urlParams: Map @annotate(type: "Map<String!, String!>")
1111
responseType: ResponseType!
12+
"""The body of the request. If present, the `formData` property will be ignored."""
1213
body: String
14+
"""
15+
An alternative to the standard request body, 'formData' is expected to be in the 'multipart/form-data' format.
16+
If present, the `body` property is not null, `formData` will be ignored.
17+
Otherwise, if formData is not null, the following header will be added to the request: 'Content-Type: multipart/form-data'.
18+
"""
19+
formData: [FormDataEntry!]
1320
timeout: UInt32
1421
}
1522

23+
type FormDataEntry {
24+
"""FormData entry key"""
25+
name: String!
26+
"""If 'type' is defined, value is treated as a base64 byte string"""
27+
value: String
28+
"""File name to report to the server"""
29+
fileName: String
30+
"""MIME type (https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types). Defaults to empty string."""
31+
type: String
32+
}
33+
1634
enum ResponseType {
1735
TEXT
1836
BINARY

packages/js/plugins/http/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@
2323
"dependencies": {
2424
"@polywrap/core-js": "0.10.0-pre.6",
2525
"@polywrap/plugin-js": "0.10.0-pre.6",
26-
"axios": "0.21.4"
26+
"axios": "0.21.4",
27+
"form-data": "4.0.0"
2728
},
2829
"devDependencies": {
2930
"@polywrap/client-js": "0.10.0-pre.6",
3031
"@polywrap/fs-plugin-js": "0.10.0-pre.6",
3132
"@polywrap/fs-resolver-plugin-js": "0.10.0-pre.6",
3233
"@polywrap/uri-resolver-extensions-js": "0.10.0-pre.6",
3334
"@polywrap/uri-resolvers-js": "0.10.0-pre.6",
35+
"@polywrap/test-env-js": "0.10.0-pre.6",
3436
"@types/jest": "26.0.8",
3537
"@types/prettier": "2.6.0",
3638
"jest": "26.6.3",

packages/js/plugins/http/src/__tests__/e2e/e2e.spec.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { UriResolver } from "@polywrap/uri-resolvers-js";
66

77
import nock from "nock";
88
import { WrapError } from "@polywrap/core-js";
9+
import { initTestEnvironment, stopTestEnvironment, providers } from "@polywrap/test-env-js";
910

1011
jest.setTimeout(360000);
1112

@@ -17,6 +18,10 @@ const defaultReplyHeaders = {
1718
describe("e2e tests for HttpPlugin", () => {
1819
let polywrapClient: PolywrapClient;
1920

21+
beforeAll(async () => {
22+
await initTestEnvironment();
23+
});
24+
2025
beforeEach(() => {
2126
polywrapClient = new PolywrapClient(
2227
{
@@ -29,6 +34,10 @@ describe("e2e tests for HttpPlugin", () => {
2934
);
3035
});
3136

37+
afterAll(async () => {
38+
await stopTestEnvironment();
39+
});
40+
3241
describe("get method", () => {
3342
test("successful request with response type as TEXT", async () => {
3443
nock("http://www.example.com")
@@ -288,5 +297,89 @@ describe("e2e tests for HttpPlugin", () => {
288297
expect(response.error).toBeDefined();
289298
expect(response.ok).toBeFalsy();
290299
});
300+
301+
test("successful request with form-data (simple)", async () => {
302+
const response = await polywrapClient.invoke<Http_Response>({
303+
uri: "wrap://ens/http.polywrap.eth",
304+
method: "post",
305+
args: {
306+
url: `${providers.ipfs}/api/v0/add`,
307+
request: {
308+
responseType: "TEXT",
309+
formData:[{
310+
name:"test.txt",
311+
value:"QSBuZXcgc2FtcGxlIGZpbGU=",
312+
fileName:"test.txt",
313+
type:"application/octet-stream"
314+
}],
315+
},
316+
},
317+
});
318+
319+
if (!response.ok) fail(response.error);
320+
expect(response.value).toBeDefined();
321+
expect(response.value?.status).toBe(200);
322+
expect(response.value?.body).toBe(JSON.stringify({
323+
Name: "test.txt",
324+
Hash: "Qmawvzw32Jq7RbMw2K8axEbzfNK74NPynBoq4tJnWvkYqP",
325+
Size: "25"
326+
}));
327+
});
328+
329+
test("successful request with form-data (complex)", async () => {
330+
const response = await polywrapClient.invoke<Http_Response>({
331+
uri: "wrap://ens/http.polywrap.eth",
332+
method: "post",
333+
args: {
334+
url: `${providers.ipfs}/api/v0/add`,
335+
request: {
336+
responseType: "TEXT",
337+
formData:[
338+
{ name: "file_0.txt", value: "ZmlsZV8w", fileName: "file_0.txt", type: "application/octet-stream" },
339+
{ name: "file_1.txt", value: "ZmlsZV8x",fileName: "file_1.txt", type: "application/octet-stream" },
340+
{ name: "directory_A", value: null, fileName: "directory_A", type: "application/x-directory" },
341+
{ name: "directory_A/file_A_0.txt", value: "ZmlsZV9BXzA=", fileName: "directory_A%2Ffile_A_0.txt", type: "application/octet-stream" },
342+
{ name: "directory_A/file_A_1.txt", value: "ZmlsZV9BXzE=", fileName: "directory_A%2Ffile_A_1.txt", type: "application/octet-stream" }
343+
],
344+
},
345+
},
346+
});
347+
348+
if (!response.ok) fail(response.error);
349+
expect(response.value).toBeDefined();
350+
expect(response.value?.status).toBe(200);
351+
352+
const results = response.value?.body?.trim()
353+
.split("\n")
354+
.map((v) => JSON.parse(v));
355+
356+
expect(results).toStrictEqual([
357+
{
358+
Name: "file_0.txt",
359+
Hash: "QmV3uDt3KhEYchouUzEbfz7FBA2c2LvNo76dxLLwJW76b1",
360+
Size: "14"
361+
},
362+
{
363+
Name: "file_1.txt",
364+
Hash: "QmYwMByE4ibjuMu2nRYRfBweJGJErjmMXfZ92srKhYfq5f",
365+
Size: "14"
366+
},
367+
{
368+
Name: "directory_A/file_A_0.txt",
369+
Hash: "QmeYp73qnn8EdogE4d6BhQCHtep7dkRC8FgdE3Qbo4nY9c",
370+
Size: "16"
371+
},
372+
{
373+
Name: "directory_A/file_A_1.txt",
374+
Hash: "QmWetZjwHWuGsDyxX6ae5wGS68mFTXC5x61H1TUNxqBXzn",
375+
Size: "16"
376+
},
377+
{
378+
Name: "directory_A",
379+
Hash: "Qmb5XsySizDeTn1kvNbyiiNy9eyg3Lb6EwGjQt7iiKBxoL",
380+
Size: "144"
381+
},
382+
]);
383+
});
291384
});
292385
});

packages/js/plugins/http/src/index.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import {
66
Http_Response,
77
manifest,
88
} from "./wrap";
9-
import { fromAxiosResponse, toAxiosRequestConfig } from "./util";
9+
import { fromAxiosResponse, toAxiosRequestConfig, toFormData } from "./util";
1010

11-
import axios from "axios";
11+
import axios, { AxiosResponse } from "axios";
1212
import { PluginFactory, PluginPackage } from "@polywrap/plugin-js";
1313

1414
type NoConfig = Record<string, never>;
@@ -29,11 +29,26 @@ export class HttpPlugin extends Module<NoConfig> {
2929
args: Args_post,
3030
_client: CoreClient
3131
): Promise<Http_Response | null> {
32-
const response = await axios.post(
33-
args.url,
34-
args.request ? args.request.body : undefined,
35-
args.request ? toAxiosRequestConfig(args.request) : undefined
36-
);
32+
let response: AxiosResponse;
33+
if (args.request?.body) {
34+
response = await axios.post(
35+
args.url,
36+
args.request.body,
37+
toAxiosRequestConfig(args.request)
38+
);
39+
} else if (args.request?.formData) {
40+
const data = toFormData(args.request.formData);
41+
const config = toAxiosRequestConfig(args.request);
42+
config.headers = {
43+
...(config.headers as Record<string, unknown>),
44+
...data.getHeaders(),
45+
};
46+
response = await axios.post(args.url, data, config);
47+
} else if (args.request) {
48+
response = await axios.post(args.url, toAxiosRequestConfig(args.request));
49+
} else {
50+
response = await axios.post(args.url);
51+
}
3752
return fromAxiosResponse(response);
3853
}
3954
}

packages/js/plugins/http/src/util.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { Http_Request, Http_Response, Http_ResponseTypeEnum } from "./wrap";
1+
import {
2+
Http_Request,
3+
Http_Response,
4+
Http_ResponseTypeEnum,
5+
Http_FormDataEntry,
6+
} from "./wrap";
27

38
import { AxiosResponse, AxiosRequestConfig } from "axios";
9+
import FormData from "form-data";
410

511
/**
612
* Convert AxiosResponse<string> to Response
@@ -90,3 +96,22 @@ export function toAxiosRequestConfig(
9096

9197
return config;
9298
}
99+
100+
export function toFormData(entries: Http_FormDataEntry[]): FormData {
101+
const fd = new FormData();
102+
entries.forEach((entry) => {
103+
const options: FormData.AppendOptions = {};
104+
options.contentType = entry.type ?? undefined;
105+
options.filename = entry.fileName ?? undefined;
106+
let value: string | Buffer | undefined;
107+
if (entry.type) {
108+
value = entry.value
109+
? Buffer.from(entry.value, "base64")
110+
: Buffer.alloc(0);
111+
} else {
112+
value = entry.value ?? undefined;
113+
}
114+
fd.append(entry.name, value, options);
115+
});
116+
return fd;
117+
}

0 commit comments

Comments
 (0)