2

Trying to add a file to an http rest call I get this error:

responseJson = {Message: "An error has occurred.", ExceptionMessage: "Invalid 'HttpContent' instance provided. It does n…rting with 'multipart/'. ↵Parameter name: content", ExceptionType: "System.ArgumentException", StackTrace: " at System.Net.Http.Formatting.Parsers.MimeMulti…tpControllerDispatcher.d__1.MoveNext()"}

my react component:

import React, { Component } from 'react';
import { Row, Col } from 'antd';
import PageHeader from '../../components/utility/pageHeader';
import Box from '../../components/utility/box';
import LayoutWrapper from '../../components/utility/layoutWrapper.js';
import ContentHolder from '../../components/utility/contentHolder';
import basicStyle from '../../settings/basicStyle';
import IntlMessages from '../../components/utility/intlMessages';
import { adalApiFetch } from '../../adalConfig';

export default class extends Component {
  constructor(props) {
    super(props);
    this.state = {TenantId: '', TenantUrl: '', TenantPassword: '' };
    this.handleChangeTenantUrl = this.handleChangeTenantUrl.bind(this);
    this.handleChangeTenantPassword = this.handleChangeTenantPassword.bind(this);
    this.handleChangeTenantId= this.handleChangeTenantId.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  };


  handleChangeTenantUrl(event){
    this.setState({TenantUrl: event.target.value});
  }

  handleChangeTenantPassword(event){
    this.setState({TenantPassword: event.target.value});
  }

  handleChangeTenantId(event){
    this.setState({TenantId: event.target.value});
  }

  handleSubmit(event){
    event.preventDefault();

    const formData = new FormData();
    formData.append("TenantId", this.state.TenantId);
    formData.append("TenantUrl", this.state.TenantUrl);
    formData.append("TenantPassword", this.state.TenantPassword);

    const options = {
      method: 'put',
      data: formData,
      config: {
        headers: {
          'Content-Type': 'multipart/form-data'
        }
      }
    };

    adalApiFetch(fetch, "/Tenant", options)
      .then(response => response.json())
      .then(responseJson => {
        if (!this.isCancelled) {
          this.setState({ data: responseJson });
        }
      })
      .catch(error => {
        console.error(error);
      });
  }


  upload(e){
      let data = new FormData();
      //Append files to form data
      let files = e.target.files;
      for (let i = 0; i < files.length; i++) {
        data.append('files', files[i], files[i].name);
      }      
  }

  render(){
    const { data } = this.state;
    const { rowStyle, colStyle, gutter } = basicStyle;

    return (
      <div>
        <LayoutWrapper>
        <PageHeader>{<IntlMessages id="pageTitles.TenantAdministration" />}</PageHeader>
        <Row style={rowStyle} gutter={gutter} justify="start">
          <Col md={12} sm={12} xs={24} style={colStyle}>
            <Box
              title={<IntlMessages id="pageTitles.TenantAdministration" />}
              subtitle={<IntlMessages id="pageTitles.TenantAdministration" />}
            >
              <ContentHolder>
              <form onSubmit={this.handleSubmit}>
                <label>
                  TenantId:
                  <input type="text" value={this.state.TenantId} onChange={this.handleChangeTenantId} />
                </label>
                <label>
                  TenantUrl:
                  <input type="text" value={this.state.TenantUrl} onChange={this.handleChangeTenantUrl} />
                </label>
                <label>
                  TenantPassword:
                  <input type="text" value={this.state.TenantPassword} onChange={this.handleChangeTenantPassword} />
                </label>
                <label>
                  Certificate:
                  <input onChange = { e => this.upload(e) } type = "file" id = "files" ref = { file => this.fileUpload } />
                </label>             
              <input type="submit" value="Submit" />
              </form>
              </ContentHolder>
            </Box>
          </Col>
        </Row>
      </LayoutWrapper>
      </div>
    );
  }
}

My tenant controller Put method

[HttpPut]
        public async Task<IHttpActionResult> PutTenant([FromBody]Tenant tenant)
        {
            var provider = new MultipartMemoryStreamProvider();
            var contentType = "";
            var content = new byte[0];
            await base.Request.Content.ReadAsMultipartAsync(provider);
            if (provider.Contents.Count > 0)
            {
                contentType = provider.Contents[0].Headers.ContentType.MediaType;
                content = await provider.Contents[0].ReadAsByteArrayAsync();
            }


            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(ConfigurationManager.AppSettings["AzureStorageKey"].ToString());
            // Create the blob client.
            CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();

            // Retrieve reference to a previously created container.
            CloudBlobContainer container = blobClient.GetContainerReference(ConfigurationManager.AppSettings["certificatesContainer"].ToString());

            // Retrieve reference to a blob named "myblob".
            CloudBlockBlob blockBlob = container.GetBlockBlobReference("myblob");

            // Create or overwrite the "myblob" blob with contents from a local file.
            blockBlob.Properties.ContentType = contentType;
            MemoryStream stream = new MemoryStream(content);
            blockBlob.UploadFromStream(stream);

            var tenantStore = CosmosStoreFactory.CreateForEntity<Tenant>();
            tenant.content = content;
            tenant.CertificatePath = blockBlob.Uri;

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var added = await tenantStore.AddAsync(tenant);
            return StatusCode(HttpStatusCode.NoContent); 
        }

and the tenant POCO

 public class Tenant
    {
        public string TenantId { get; set; }
        public string TenantUrl { get; set; }
        public Uri CertificatePath { get; set; }
        public string CertificatePassword { get; set; }

        public byte[] content { get; set; }

        public override string ToString()
        {
            return JsonConvert.SerializeObject(this);
        }
    }

stacktrace:

"   at System.Net.Http.Formatting.Parsers.MimeMultipartBodyPartParser.ValidateArguments(HttpContent content, Int64 maxMessageSize, Boolean throwOnError)
   at System.Net.Http.Formatting.Parsers.MimeMultipartBodyPartParser..ctor(HttpContent content, MultipartStreamProvider streamProvider, Int64 maxMessageSize, Int32 maxBodyPartHeaderSize)
   at System.Net.Http.HttpContentMultipartExtensions.<ReadAsMultipartAsync>d__0`1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at TenantManagementWebApi.Controllers.TenantController.<PutTenant>d__2.MoveNext() in C:\Users\levm3\source\repos\TenantManagementWebApi\Controllers\TenantController.cs:line 48
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Threading.Tasks.TaskHelpersExtensions.<CastToObject>d__3`1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.AuthorizationFilterAttribute.<ExecuteAuthorizationFilterAsyncCore>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()"

Update 1:

When I attach the debugger remotely the tenant parameter is null, so something must be wrong when setting the parameters on the client component.

Luis Valencia
  • 32,619
  • 93
  • 286
  • 506

3 Answers3

1

The error message is telling you it doesn't like the way you formatted the data. From experience, C# MVC and API does not like form data. I would change your content type to 'application/json', then send the data like below:

data:{"TenantId": this.state.TenantId, "TenantUrl": this.state.TenantUrl, "TenantPassword": this.state.TenantPassword }

Also, double check that if the table you're inserting into contains any not null fields, those fields are not null when you perform the insert.

  • are you sure this will work? application/json? I am also sending a fileupload. – Luis Valencia Jul 17 '18 at 07:15
  • this answer is not valid, it doesnt work, I tried changing the contenttype, and then the error says its expectint multiform/data because I am expecting a file upload. I do changed data as you said. – Luis Valencia Jul 18 '18 at 05:41
  • You'd need to change your React side to also construct a json message with the file upload data in it, likely needing to be base64 encoded which would increase the payload size somewhat – Thymine Jul 19 '18 at 13:56
1

WebApi doesn't support FormMultipart data very well, you need to basically parse it manually: https://stackoverflow.com/a/28462699/356218

I thought I recalled having to register handlers for multipart/form-data so that your server even knows it can try to handle that datatype, but the above as well as https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/sending-html-form-data-part-2 (pretty similar to the above link) doesn't seem to mention that

Thymine
  • 8,775
  • 2
  • 35
  • 47
1

I guess your [FromBody]Tenant tenant is null because you use multipart/form-data.

The default model binder can not resolve your model in this case.

Please review example from Microsoft documentation (section Reading Form Control Data).

You need to read your model data using MultipartFormDataStreamProvider. As I can see you use the provider only for file content reading. But also you can read your model data (like TenantId, TenantUrl, etc) from FormData property.

As second (and more general) approach you can create your own IModelBinder implementation.

Here you can review example of my implementation of IModelBinder (It is code from real project):

public class NativeTranslateModelBinder : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(NativeTranslateViewModel))
        {
            return false;
        }

        var task = Task.Run(async () =>
        {
            var model = new NativeTranslateViewModel();

            if (!actionContext.Request.Content.IsMimeMultipartContent())
            {
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, "WebRequeest content 'multipart/form-data' is valid");
            }
            else
            {
                var provider = await actionContext.Request.Content.ReadAsMultipartAsync();

                var fileContent = provider.Contents.FirstOrDefault(n => n.Headers.ContentDisposition.Name.Equals("file"));
                if (fileContent == null)
                {
                    bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Section 'file' is missed");
                }

                var modelContent = provider.Contents.FirstOrDefault(n => n.Headers.ContentDisposition.Name.Equals("model"));
                if (modelContent == null)
                {
                    bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Section 'model' is missed");
                }

                if (fileContent != null && modelContent != null)
                {
                    model = JsonConvert.DeserializeObject<NativeTranslateViewModel>(await modelContent.ReadAsStringAsync());
                    model.Text = "<NativeTranslation>";
                    model.FileData = await fileContent.ReadAsByteArrayAsync();
                    model.FileName = fileContent.Headers.ContentDisposition.FileName;
                }
            }

            return model;
        });

        task.Wait();

        bindingContext.Model = task.Result;
        return true;
    }
}

We can use new model binder very easy:

[HttpPost]
public HttpResponseMessage UploadNativeDocument([ModelBinder(typeof(NativeTranslateModelBinder))] NativeTranslateViewModel model)

Now we can use our model and model properties inside controller action.

Alexander I.
  • 2,380
  • 3
  • 17
  • 42
  • so I dont need to change my react code, my problem is in the web api only, I thought I was sending the parameters wrong – Luis Valencia Jul 19 '18 at 09:45
  • I think you have a trouble with web api. Review documentation link from my answer. Just try to read all model properties using `MultipartFormDataStreamProvider`. – Alexander I. Jul 19 '18 at 09:47
  • then I would need a custom ModelBinder each time I have a form with a file? for standard forms I dont need a model binder right? – Luis Valencia Jul 19 '18 at 10:18
  • You can implement model binder only for `multipart/form-data` requests. If you send only form you can send JSON or Form. In this case you do not need ModelBinder. Example for sending form is [here](https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/sending-html-form-data-part-1) – Alexander I. Jul 19 '18 at 10:41
  • @Alexamder, I did exactly as you said, but actionContext.Request.Content.IsMimeMultipartContent() is false, – Luis Valencia Jul 19 '18 at 15:45
  • please check this: https://stackoverflow.com/questions/51427280/modelbinder-ismultipartcontent-always-returning-false-how-to-upload-file-with-r – Luis Valencia Jul 19 '18 at 16:20
  • your code has helped, however I am still struggling, can u see this one: https://stackoverflow.com/questions/51430809/webapi-how-to-read-the-file-from-a-post-put-action-with-custom-model-binder – Luis Valencia Jul 19 '18 at 20:21