0

My project is setup as follows:
Frontend - TypeScript, React, fetch() for calling my backend API as opposed to axios.
Backend - C#, ASP .NET Core, Swagger UI, Azure

I am trying to implement a simple upload feature of image files to my S3 bucket via my own API backend deployed on Azure. When I test just the API endpoint for uploading with Swagger UI, it works without any issues. However, when I try calling the same endpoint from my frontend with fetch() I get the following error:

ImageUploadFunction.tsx:33     POST https://project.azurewebsites.net/api/files/upload 500 (Internal Server Error)

Backend code: FilesController.cs

using System.Threading.Tasks;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System;
using System.Linq;
using API.Model;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;

namespace API.Controllers
{
    [Route("api/files")]
    [ApiController]
    public class FilesController : BaseApiController
    {
        private readonly IAmazonS3 _s3Client;
        private readonly IOptions<AWSSecretsModel> awsSettings;
        private readonly IConfiguration _config;

        public FilesController(IAmazonS3 s3Client, IOptions<AWSSecretsModel> aws, IConfiguration config)
        {
            _s3Client = s3Client;
            awsSettings = aws;
            _config = config;
        }

        [HttpPost("upload")]
        public async Task<IActionResult> UploadFileAsync(IFormFile file)
        {
            if (file is null)
            {
                throw new ArgumentNullException(nameof(file));
            }

            try 
            {
                var awsBucketName = awsSettings.Value.AWS_S3_BUCKET;
                var awsRegion = awsSettings.Value.AWS_REGION;

                // Upload the raw image to S3
                var bucketExists = await _s3Client.DoesS3BucketExistAsync(awsBucketName);
                if (!bucketExists) return NotFound($"Bucket {awsBucketName} does not exist.");

                var fileNameToPrefix = file.FileName;

                var request = new PutObjectRequest
                {
                    BucketName = awsBucketName,
                    Key = fileNameToPrefix,
                    InputStream = file.OpenReadStream()
                };
                await _s3Client.PutObjectAsync(request);

                return Ok($"File {file.FileName} [{fileNameToPrefix}] uploaded to S3 successfully!");
            }
            catch (ArgumentNullException ex)
            {
                return BadRequest(ex.Message);
            }
            catch (AmazonS3Exception ex)
            {
                return BadRequest($"Error uploading to S3: {ex.Message}");
            }
            catch (Exception ex)
            {
                return BadRequest($"Error: {ex.Message}");
            }
        }
    }
}

Frontend: ImageUploadFunction.tsx

import { useState, useEffect, ChangeEvent } from "react";

const API_URL = process.env.REACT_APP_API_URL;
const UPLOAD_URL = API_URL + `/api/files/upload`;

const ImageUpload: React.FC = () => {
    const [selectedImage, setSelectedImage] = useState<File | null>(null);
    const [uploading, setUploading] = useState(false);

    const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
        const files = event.target.files;
        if (files && files.length > 0) {
            setSelectedImage(files[0]);
        }
    };

    const handleImageUpload = async () => {
        if (!selectedImage) return;

        try {
            setUploading(true);

            const formData = new FormData();
            formData.append('file', selectedImage);

            const response = await fetch(UPLOAD_URL, {
                method: 'POST',
                body: formData,
            });

            if (response.ok) {
                console.log('Image uploaded successfully!');
            } else {
                console.error('Image upload failed!');
            }
        } catch (error) {
            console.error('Error uploading image:', error);
        } finally {
            setUploading(false);
        }
    };

    return (
        <div>
            <input type="file" onChange={handleFileChange} />
            <button onClick={handleImageUpload} disabled={!selectedImage || uploading}>
                {uploading ? 'Uploading...' : 'Upload Image'}
            </button>
        </div>
    );
};

export default ImageUpload;

Since everything works fine via Swagger and Postman, I feel like the problem has something to do with my frontend, despite the error message. I just can't see what I'm doing wrong.

Any suggestions?

EDIT:
I just noticed that I am only able successfully upload an image file through localhost when I run my backend in its development environment (https://localhost:5001/api/files/upload). Trying to do a POST call via my live app URL (https://project.azurewebsites.net/api/files/upload) gives me the 500 internal server error. So my issue definitely lies in my Azure configuration, right?

EDIT 2:
Below is my bucket policy as well as my CORS configuration for my S3 Bucket.

Bucket policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicRead",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::mys3bucketname/*"
        }
    ]
}

CORS configuration:

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT",
            "POST",
            "HEAD"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]

EDIT 3: Added exception handling in backend code (FilesController.cs) as suggested in comments.

malthe.w
  • 77
  • 10
  • How is the access key and secret set in azure and how is this application deployed in azure – Vikram S Jun 20 '23 at 01:40
  • Thanks for your comment @VikramS. I have an `"AWS: {}"` field in my `appsettings.json` file with all of the credentials (bucket, access key id, secret access key, profile, region). I deploy my API via the Azure extension for VSC ("Deploy to Web App..."). – malthe.w Jun 20 '23 at 06:47
  • Do you have CORS configured on your Bucket? Than you might have a look here https://stackoverflow.com/a/19557452/5341567 – Max Jun 20 '23 at 07:54
  • @Max Yes, I believe I do. Please see the edit I've just made to my question, where I include both my bucket policy as well as my CORS config. – malthe.w Jun 20 '23 at 08:13
  • 2
    A 500 errror status suggests, there is an exception happening in your `"upload"` endpoint. Add some exception handling and return the respective error message and maybe even a stacktrace so you can see *what* actually is going wrong. When you say it works via Postman do you mean it works for localhost only or also for your deployment on azure? – derpirscher Jun 20 '23 at 14:52
  • @derpirscher Correct. The project works only for localhost and not for my deployment on azure. – malthe.w Jun 21 '23 at 06:21
  • 1
    I agree with @derpirscher your upload endpoint is probably throwing an exception. A default trace is logged in the console, in Azure this is probably redirected somewhere. You will have more information on which line is failing and why. – MarcC Jun 21 '23 at 21:46
  • I've added some exception handling to my upload endpoint. Now I'm getting a `400 (Bad Request)` error response, with no additional information. I've included the changes I made in my question above. – malthe.w Jun 22 '23 at 09:55
  • 1
    That you now receive a 400 shows, there is some exception happening, that is now caught, because you return a `BadRequest` in your `catch` clauses. As I'm not working with ASP.Net I'm not sure how that `BadRequest("foobar")` is working, but somewhere in the response the "foobar" string should be included. But in your frontentcode, you never check the body of the response, just `response.ok`. Did you try using the network inspector of your browser to inspect that response? – derpirscher Jun 22 '23 at 11:29
  • @derpirscher I just tried adding the body of the response to the error message in my frontend code with both `console.error('Image upload failed:', response.statusText);` as well as `console.error('Image upload failed:', response.json());`. First one just outputted `Image upload failed: Bad request.` while using `response.json()` outputted the following: `Image upload failed: Uncaught (in promise) SyntaxError: Unexpected token 'A', "An error o"... is not valid JSON`. Not sure if that's useful at all? – malthe.w Jun 27 '23 at 09:29
  • 1
    Don't juse `response.json()` if you are not sure your response body is actually json (and obviously it isn't, because it starts with `"An error"` Use `response.text()` or use the browsers developer tools to inspect the response ... and furthermore `response.json()` and `response.text()` are `async` so you have to `await` them before logging their result to the console ... – derpirscher Jun 27 '23 at 09:30
  • @derpirscher That makes total sense. I didn't even know that function existed. As you can tell I'm a pretty new developer... This is the error message I get now: `Image upload failed: "An error occurred: Unable to get IAM security credentials from EC2 Instance Metadata Service."` Which obviously points to there being an issue with how I provide AWS credentials to my backend or the profile's authorization settings. Only weird part is how EBS doesn't throw this error even though both Azure and EBS are using the exact same `appsettings.json` file and AWS profile. – malthe.w Jun 27 '23 at 09:49
  • 1
    I never worked with EBS but it might have something to do, that this way you are already within the AWS ecosystem and EBS may somehow be able to acquire some working credentials even if the profile provided in the settings isn't valid. For Azure this of course won't work .. – derpirscher Jun 27 '23 at 09:57

1 Answers1

0

Not really a true solution to the actual problem, but I decided to switch to AWS Elastic Beanstalks instead, given that all of my other services are hosted by AWS. I can already reveal that after deploying my backend straight to EBS, my upload endpoint works like a charm.

However, as both Derpirscher and Marc Campora have pointed out, providing exception handling in endpoints to show what might be going on could (and honestly should) point one in right direction. In my case though, this did not help me any further and so I've just decided to bite the bullet and move away from Azure.

I'm still keeping all Azure related code and so I'll happily leave this question open for anyone else to come with suggestions/solutions and I'll try to implement them. Just in case anyone else might stumble across this issue as well.

malthe.w
  • 77
  • 10