Problem with uploading files from TypeScript

Hello,

Just in case, I am still quite new to ServiceStack.

I am working on a form with file upload and for some reason the sent files do not appear in the Request.Files in our webservice.

In the frontent we’re using SvelteKit and TypeScript. I am trying to send the form as FormData, following the guidelines from the documentation: Managed Files Uploads

First of all, I noticed that the files never get to the webservice - Request.Files always shows 0.
I found out that FormData with files should be sent as ‘multipart/form-data’ content type. However, inspecting the Request shows ‘application/json’ instead. The payload in the browser’s dev tools shows that the request has the ‘multipart/form-data’ content type, which makes me believe that everything on the frontend side should be correct. We even tried to set the default content type to ‘MultiPartFormData’ in our webservice, but the result is still the same - no files received and a different content type.

I have no idea where the problem is and what I am missing/misunderstanding. Can you please help me with this problem?

This is the issue, the request needs to be made with the ‘multipart/form-data’ content type, which is what browsers send natively in HTML forms using:

<form method="post" enctype="multipart/form-data">
    <input type="file" />
</form>

To upload files with JavaScript you would use fetch with FormData as seen in Mozilla’s Uploading Files docs.

If using the TypeScript JsonServiceClient you would send it with apiForm as documented in Uploading Files from JS/TypeScript docs.

const client = JsonApiClient.create(BaseUrl)
const formData = new FormData(document.forms[0]) // HTML Form Element
const api = await client.apiForm(new MyRequest(), formData)

Hello,

Than you for answering!

I already have ‘multipart/form-data’ in my form and I used the apiForm you mentioned. The only difference I can see is that our ‘client’ is ‘JsonServiceClient’, not ‘JsonApiClient’. Is that a significant difference?

For some reason, I can’t import ‘JsonApiClient’ and I can access only the ‘JsonServiceClient’.

import { JsonApiClient } from "@servicestack/client"
const client = JsonApiClient.create(baseUrl)

Is equivalent to:

let client = new JsonServiceClient(baseUrl).apply(c => {
    c.basePath = "/api"
    c.headers = new Headers() //avoid pre-flight CORS requests
})

Where it just configures the JsonServiceClient instance to use the JSON API Route to avoid unnecessary CORS pre-flight requests when not needed.

If you don’t have JsonApiClient make sure you’re referencing the latest version of @servicestack/client:

"dependencies": {
  "@servicestack/client": "^2.0.10",
 }

If you’re using apiForm populated with FormData it should be sending multipart/form-data requests, inspect the FormData instance (e.g. Array.from(formData.keys())) to ensure it has the File Input id, then have a look at the Raw HTTP Headers in Web Inspector to see what’s actually being sent.

Note:

That’s only required when you’re submitting the HTML Form natively, not when you’re using JavaScript in which case you’ll instead need to use apiForm, e.g:

const formData = new FormData(formElement)
const api = await client.apiForm(new MyRequest(), formData)

Ok, I removed the ‘multipart/form-data’ from the form.

About what is actually being sent:
It looks like my formData is populated correctly, I can see the input’s id in the console.
The content type in the headers is ‘multipart/form-data’.
When I’m checking the payload, I can see this:

That looks right, in which case Request.Files should now be populated

The thing is that it’s still empty

Request.Files is basically a wrapper over the underlying the Uploaded Files collection that’s populated by the underlying HTTP Framework, there’s limited control other than ensuring the HTTP Request is properly sent.

If you can provide the full HTTP Request and Response Headers in WebInspector and the C# Request DTO hopefully it will provide some insights otherwise if you can provide a minimal stand-alone repro I’ll be able to let you know what the issue is.

Here are the Request and Response Headers:

Our C# Request DTO is empty. I read somewhere that there is no need to specify anything in it when using the FormData. But maybe I’m wrong.

If you’re using the JSON API Route your Route is incorrect, it should be:

https://localhost:3000/api/{RequestDtoName}

i.e. It should be using the Request DTO name not the /messages/send custom Route, but this should be automatically handled with apiForm(new MyRequest(), formData), what JavaScript code are you using to send the request?

Also if you’re going through a npm HTTP Proxy that could potentially be causing the issue if it doesn’t proxy multi-part requests properly, i.e. if your .NET API is running on https://localhost:5001 you can try sending a request directly to the .NET API:

import { JsonApiClient } from "@servicestack/client"
const client = JsonApiClient.create("https://localhost:5001")

const formData = new FormData(formElement)
const api = await client.apiForm(new MyRequest(), formData)

But if you’ll need to enable CORS in order to perform cross-domain requests.

Hello,

Sorry for answering late.

About the route:

It’s a bit complicated. We’re ‘issuing’ the request from Svelte component like this:

const formData = new FormData(document.forms[0]);
const res = await sendMessage(formData);

‘sendMessage’ is imported from another file and it looks as follows:

export const sendMessage = async (formData: FormData): Promise<Response> => {
	const options = {
		method: 'POST',
		body: formData,
	};

	const res = await fetch(`${baseApiUrl}/messages/send`, options);

	return res;
};

I believe what appears in the HTTP Request headers is the url we’re using in the fetch above. The thing is, we’re using it to pass the data on to +server.ts (it’s a SvelteKit file, it runs on the server side), where we use the apiForm:

export const POST: RequestHandler = async ({ request, locals }) => {
	client.headers.set('X-ss-id', locals.ssid ?? null);
	client.headers.set('X-ss-opts', 'temp');

	const formData = await request.formData();

	const sendMessage: Message = await client.apiForm(new CreateMessage(), formData);

Here is CreateMessage():

// @Route("/message", "POST")
export class CreateMessage implements IReturn<Message> {
		public constructor(init?: Partial<CreateMessage>) {
		(Object as any).assign(this, init);
	}
	public createResponse() {
		return new Message();
	}
	public getTypeName() {
		return 'CreateMessage';
	}
}

From what I found out, we are already sending the request directly to the .NET API. All other requests work well, we only have a problem with this one. But it’s also the only case where we’re using the multipart/form-data content

So in this case you’re not using the TypeScript JsonServiceClient here and are instead using fetch directly in which case Mozilla’s Uploading a file with fetch would be more relevant.

The Route for the Request DTO suggests its custom route is /message but you’re trying to send the request to /api/messages/send which I don’t see the Request DTO for.

I don’t really know what’s going on here in your POST RequestHandler which appears is manually setting session ids instead of authenticating the client normally and it’s not clear what request.formData() is returning, but if it’s a valid FormData then the JsonServiceClient request should work:

const sendMessage: Message = await client.apiForm(new CreateMessage(), formData);

But you’ve only send the HTTP Headers for the /messages/send request which isn’t the CreateMessage request. What are the HTTP Headers for the CreateMessage request sent with the JsonServiceClient and does it populate Request.Files?

We’re using the fetch in ‘sendMessage’ for internal routing in SvelteKit. I might have sent that part unnecessairly, it doesn’t have much to do with the ServiceStack. Sorry for that.

About the request.formData(): it’s just the form data delivered by the fetch. These are just the values that I wrote in the form and it is valid FormData.

Here are the headers for the CreateMessage request:

It looks like it does not populate Request.Files. And the content type is not multipart/form-data

You’ve been sending information for the wrong request in that case, you’ll need to check the keys to ensure the uploaded file is included in the FormData sent (e.g. Array.from(formData.keys())).

If it does, what are the WebInspector HTTP Request / Response Headers that the client sends for the CreateMessage request?

Hi, I’m also working on this.

The CreateMessage request is sent from the sveltekit backend, we cannot see the headers in the browser directly.

How are you verifying the client is sending the file upload with the HTTP Request?

If you’re not viewing it from the browser you’d need to use a HTTP Inspector like WireShark, Fiddler or HTTP Toolkit, etc.

By printing the content of formData in the terminal. Here’s the function:

const formData = await request.formData();

	for(const value of formData.values()) {
		console.log(value);
	}

Here’s the output:

image

ok next step would be verifying the HTTP Request the client sends with a HTTP Inspector.

I’ll get back to you on Friday