Uploading Files to Laravel Vapor (w/ Nuxt combo)

Hello!

Re-factored a bunch of my app with everything from your Laravel API book and it’s going GREAT! Faster, easier to read, efficient - love it.

Running into a bit of a snag with Laravel Vapor - which I understand you might not know fully - but since I’m running into a CSRF issue, I’m pretty sure you might have some ideas.

You can read here in the Laravel Vapor Docs about File Uploads. Essentially, it consists of loading a package and then making this call, to temporarily store your file on AWS:

Vapor.store(this.$refs.file.files[0], {
    progress: progress => {
        this.uploadProgress = Math.round(progress * 100);
    }
}).then(response => {
    axios.post('/api/profile-photo', {
        uuid: response.uuid,
        key: response.key,
        bucket: response.bucket,
        name: this.$refs.file.files[0].name,
        content_type: this.$refs.file.files[0].type,
    })
});

My problem is this - it works GREAT locally, I can upload, do whatever I want with, etc. When I upload it to production it fails.

At first it was calling the wrong domain by posting to the frontend domain, instead of the API domain. So I added a baseURL option:

 Vapor.store(this.$refs.file.files[0], {
          progress: progress => {
              this.uploadProgress = Math.round(progress * 100);
          },
          baseURL: this.storageUrl,
      })

where storageURL is my Laravel API server.

That got rid of the 404, but NOW I’m getting the dreaded 419 CSRF token mismatch that users of Laravel Sanctum are so familiar with :slight_smile:

I tried adding this as an option as well:

          headers: {
            credentials: true,
            withCredentials: true
          }  

with no joy.

Any ideas??

The relevant file from the laravel-vapor npm repo just looks like this:

    async store(file, options = {}) {
        const response = await axios.post('/vapor/signed-storage-url', {
            'bucket': options.bucket || '',
            'content_type': options.contentType || file.type,
            'expires': options.expires || '',
            'visibility': options.visibility || ''
        }, {
            baseURL: options.baseURL || null,
            headers: options.headers || {},
            ...options.options
        });

Thank you in advance for your expertise!

1 Like

Hi @jhull,

I’m so happy the book is helping with your app! We will be doing another update soon as well.

To be open and transparent, I’ve haven’t done anything with Laravel Vapor, but do have a few ideas that might be causing the issue.

Here’s my understanding of what is going on.

  1. Your file stream to S3 Works. You get a temporary file in your S3 bucket.
  2. The API returns a 419 when you try to take the response from Vapor.store() method and make a call to your API /api/profile-photo endpoint.

If I understand correctly, this my idea for the solution would be to use the NuxtJS axios module $axios instead of axios.

In your callback, you are making the reference to axios instead of this.$axios. this.$axios contains your proper CSRF token configuration used by NuxtJS to communicate to your API. The example would look kind of like this with NuxtJS:

Vapor.store(this.$refs.file.files[0], {
    progress: progress => {
        this.uploadProgress = Math.round(progress * 100);
    }
}).then(response => {
    this.$axios.post('/api/profile-photo', {
        uuid: response.uuid,
        key: response.key,
        bucket: response.bucket,
        name: this.$refs.file.files[0].name,
        content_type: this.$refs.file.files[0].type,
    })
}.bind(this));

If you check your network inspector, I would guess that the X-XSRF-TOKEN is not being passed correctly through Axios and this is due to not using the NuxtJS axios module.

To debug, I’d first check to see if the X-XSRF-TOKEN is being sent with the callback to your API after Vapor finishes. If it is being sent, I’d make a simple request to your API through your app and see if the tokens match.

The other possibility is that the Vapor object has it’s own scoped CSRF-TOKEN that gets sent through axios when uploading to AWS. This could be adding THAT token to the request to your API since it’s within a callback.

Let me know how this works and if you need a hand with anything else!

It is indeed not being sent correctly.

The issue is in the first Vapor.store call, which then makes this call:

    async store(file, options = {}) {
        const response = await axios.post('/vapor/signed-storage-url', {
            'bucket': options.bucket || '',

That is in the laravel-vapor npm package - how would I go about forcing that axios call there to use the Nuxt version of axios? (since I can’t edit it directly)

The Vapor.store call is done through the Vapor package and that’s to your AWS account correct? It’s the callback request to your API /api/profile-photo in the callback that is failing with the 419 error code if I understand correctly. This call attempts to POST to /api/profile-photo with the response from your AWS Vapor call.

To use NuxtJS’ Axios in the callback, it should look like:

.then(response => {
    this.$axios.post('/api/profile-photo', {
        uuid: response.uuid,
        key: response.key,
        bucket: response.bucket,
        name: this.$refs.file.files[0].name,
        content_type: this.$refs.file.files[0].type,
    })
}.bind(this))

When you use this.$axios it will reference the NuxtJS module which will have all of your credentials configured when you authenticated with Laravel Sanctum and apply that to the request.

If the issue IS with the Vapor.store() request and not the callback, I’d assume it’d have to be something with the headers you pass in as your second parameter. The second parameter uses whatever you pass in and overwrites any of the config. That means if you are missing a header that applies the X-XSRF-TOKEN to the Vapor request and it’d throw a 419. How do you store your AWS credentials? On production there has to be some discrepancy compared to local. Are there any domains you have to verify on AWS? I haven’t done much with S3 + Vapor so let me know if I’m thinking along the same lines. We will get past this dreaded 419!

Thanks Dan,

It’s the Vapor.store() request. I got this from Mohammed:

You can replicate Vapor.store into your own project and customize the behaviour as you like. It was mainly done to cover 90% of the use cases. We recommend people implement your own JS uploading logic if their use cases fall into the 10%.

Which I admit, I’m not 100% sure how to carry out.

Is it just enough, you think, to copy the code from laravel-vapor repo into my component?

The following is the relevant code from that repo:

    async store(file, options = {}) {
        const response = await axios.post('/vapor/signed-storage-url', {
            'bucket': options.bucket || '',
            'content_type': options.contentType || file.type,
            'expires': options.expires || '',
            'visibility': options.visibility || ''
        }, {
            baseURL: options.baseURL || null,
            headers: options.headers || {},
            ...options.options
        });

Probably an easy question which I can try out myself, but is it just enough to copy this over into the component?

Figured it out.

Turns out, it was easiest to copy the following into one of the API plugins used in your book’s methodology (approach):

  async uploadVaporFile(file, options = {}) {
      const response = await $axios.post('/vapor/signed-storage-url', {
        'bucket': options.bucket || '',
        'content_type': options.contentType || file.type,
        'expires': options.expires || '',
        'visibility': options.visibility || ''
    }, {
        // baseURL: options.baseURL || null,
        headers: options.headers || {},
        ...options.options
    });

    let headers = response.data.headers;

    if ('Host' in headers) {
        delete headers.Host;
    }

    if (typeof options.progress === 'undefined') {
        options.progress = () => {};
    }

    const cancelToken = options.cancelToken || ''

    await $axios.put(response.data.url, file, {
        cancelToken: cancelToken,
        headers: headers,
        onUploadProgress: (progressEvent) => {
            options.progress(progressEvent.loaded / progressEvent.total);
        }
    })

    response.data.extension = file.name.split('.').pop()

    return response.data;
  }

And all I did was just add that $ sign to the front of the two axios calls.

Then, all I have to do in my component is make this call instead of the Vapor.store call:

      this.$api.files.uploadVaporFile(this.$refs.file.files[0], {
          progress: progress => {
              this.uploadProgress = Math.round(progress * 100);
          },
      }).then(response => { ...

(assuming a files.api file…)

Pure magic, so thank you :smiley:

Hi @jhull,

Thank you so much for sharing the solution and I’m glad you got it figured out! I was doing a project recently with 2 APIs and finding some of the ways to balance the configuration in a maintainable way. It’s super tricky!

Let us know if you have any more questions and I’ll do my best to help!