I'm developing a plugin for Obsidian.md that needs to make an HTTP request to an API that expects a file in a multipart/form-data
payload. In order to ensure the plugin works on mobile, I have to use the requestUrl
method provided by Obsidian's TypeScript lib in order to ensure the plugin will work on mobile, among some other compatibility reasons.
However, requestUrl
expects the body to be a string
or an ArrayBuffer
- is there a way to convert FormData
into a string
or ArrayBuffer
? Or am I taking the wrong approach to constructing the payload?
The interface: https://marcus.se.net/obsidian-plugin-docs/reference/typescript/interfaces/RequestUrlParam
/** @public */
export interface RequestUrlParam {
/** @public */
url: string;
/** @public */
method?: string;
/** @public */
contentType?: string;
/** @public */
body?: string | ArrayBuffer;
/** @public */
headers?: Record<string, string>;
/**
* Whether to throw an error when the status code is >= 400
* Defaults to true
* @public
*/
throw?: boolean;
}
My code:
const formData = new FormData();
const data = new Blob([await this.app.vault.adapter.readBinary(fileToTranscribe.path)]);
formData.append("audio_file", data);
const options: RequestUrlParam = {
method: "POST",
url: "http://djmango-bruh:9000/asr?task=transcribe&language=en",
contentType: "multipart/form-data",
body: formData,
};
requestUrl(options).then((response) => {
console.log(response);
}).
catch((error) => {
console.error(error);
});
Current error:
Type 'FormData' is not assignable to type 'string | ArrayBuffer | undefined'.
Additional References: API Repo API Docs A working implementation of the method
CodePudding user response:
I've built a workaround, essentially I just inspected the valid requests from fetch/Postman and am now constructing the multipart/form-data payload in an identical format manually.
There may be an existing library or util function to do this in a cleaner way, if I don't find one I may write one myself if I need to send file payload requests in other parts of my plugin.
// This next block is a workaround to current Obsidian API limitations: requestURL only supports string data or an unnamed blob, not key-value formdata
// Essentially what we're doing here is constructing a multipart/form-data payload manually as a string and then passing it to requestURL
// I believe this to be equivilent to the following curl command: curl --location --request POST 'http://djmango-bruh:9000/asr?task=transcribe&language=en' --form 'audio_file=@"test-vault/02 Files/Recording.webm"'
// Generate the form data payload boundry string, it can be arbitrary, I'm just using a random string here
// https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data
// https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript
const N = 16 // The length of our random boundry string
const randomBoundryString = "djmangoBoundry" Array(N 1).join((Math.random().toString(36) '00000000000000000').slice(2, 18)).slice(0, N)
// Construct the form data payload as a string
const pre_string = `------${randomBoundryString}\r\nContent-Disposition: form-data; name="audio_file"; filename="blob"\r\nContent-Type: "application/octet-stream"\r\n\r\n`;
const post_string = `\r\n------${randomBoundryString}--`
// Convert the form data payload to a blob by concatenating the pre_string, the file data, and the post_string, and then return the blob as an array buffer
const pre_string_encoded = new TextEncoder().encode(pre_string);
const data = new Blob([await this.app.vault.adapter.readBinary(fileToTranscribe.path)]);
const post_string_encoded = new TextEncoder().encode(post_string);
const concatenated = await new Blob([pre_string_encoded, await getBlobArrayBuffer(data), post_string_encoded]).arrayBuffer()
// Now that we have the form data payload as an array buffer, we can pass it to requestURL
// We also need to set the content type to multipart/form-data and pass in the boundry string
const options: RequestUrlParam = {
method: 'POST',
url: 'http://djmango-bruh:9000/asr?task=transcribe&language=en',
contentType: `multipart/form-data; boundary=----${randomBoundryString}`,
body: concatenated
};
requestUrl(options).then((response) => {
console.log(response);
}).catch((error) => {
console.error(error);
});
Full source here