0

With reference to ASP.NET Core form POST results in a HTTP 415 Unsupported Media Type response,

I've been attempting to make API changes (Changing the Data annotations, changing the path, explicitly declaring the params with data annotations and even making sure asp-action does the right thing on the view.)

But to no avail, here's the stack trace.

[40m[32minfo[39m[22m[49m: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
      Executed action CounterCore.Controllers.AdvertController.Modify (CounterCore) in 70.696ms
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action CounterCore.Controllers.AdvertController.Modify (CounterCore) in 70.696ms
[40m[32minfo[39m[22m[49m: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
      Executed action CounterCore.Controllers.AdvertController.Modify (CounterCore) in 70.696ms
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action CounterCore.Controllers.AdvertController.Modify (CounterCore) in 70.696ms
Exception thrown: 'System.InvalidOperationException' in System.Private.CoreLib.dll
[40m[32minfo[39m[22m[49m: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 1536.417ms 200 
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 1536.417ms 200 
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 1536.417ms 200 

Notice something?

Exception thrown: 'System.InvalidOperationException' in System.Private.CoreLib.dll

Doesn't give me any useful information to tap on...

The view is generated from this method:

[Authorize]
[HttpGet("{id}")]
public async Task<IActionResult> Modify(long id)

That returns the ViewModel,

Which will POST to this method via the standard Razor view:

[Authorize]
[HttpPost("{id}")]
public async Task<IActionResult> Modify(long id,[FromForm]ModifyViewModel model)

I did remove the FromForm and all that kinda stuff, nothing works. It returns me a useless one liner Exception..

View:

<form class="form side-gap rounded" id="form" asp-route-returnUrl="@ViewData["ReturnUrl"]" method="post">
        <div asp-validation-summary="All" class="text-danger"></div>

        <h3>Edit your advert</h3>

        <hr class="my-5" />

        <div class="form-group row">
            <h5 for="typeDropdown" class="col-sm-2 col-form-label">I want to</h5>
            <div class="col-sm-10 input-group">
                <div id="typeDropdown" class="dropdown">
                    <button type="button" id="typeDropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-primary dropdown-toggle">
                        @Model.AdvertType.Name
                    </button>
                    <div aria-labelledby="dropdownMenuButton" id="typeDropdownMenu" class="dropdown-menu type">
                        @{
                            foreach (var advertType in Model.AdvertTypes)
                            {
                                Output.Write("<a class=\"dropdown-item type\" value=\"{1}\">{0}</a>", advertType.Name, advertType.Id);
                            }
                        }
                    </div>
                </div>
                <input asp-for="AdvertTypeId" id="typeDropdownInput" type="hidden" />
            </div>
        </div>

        <div class="form-group row">
            <h5 for="walletTypeDropdown" class="col-sm-2 col-form-label">Cryptocurrency</h5>
            <div class="col-sm-10 input-group">
                <div id="walletTypeDropdown" class="dropdown">
                    <button type="button" id="walletTypeDropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-primary dropdown-toggle">
                        @Model.WalletType.Name
                    </button>
                    <div aria-labelledby="walletTypeButton" id="walletTypeDropdownMenu" class="dropdown-menu walletType">
                        @{
                            foreach (var walletType in Model.WalletTypes)
                            {
                                Output.Write("<a class=\"dropdown-item type\" value=\"{1}\">{0} ({2})</a>", walletType.Name, walletType.Id, walletType.CurrencyName);
                            }
                        }
                    </div>
                </div>
                <input asp-for="WalletTypeId" id="walletTypeDropdownInput" type="hidden" />
            </div>
        </div>

        <hr class="my-3" />

        <div class="form-group row">
            <h5 class="col-sm-2 col-form-label">Location</h5>
            <div class="col-sm-4">
                <input id="locationInput" type="text" class="form-control" />
                <input asp-for="MeetingPlace" id="locationInputValue" type="hidden" />
            </div>
        </div>

        <hr class="my-3" />

        <div class="form-group row">
            <h5 for="paymentTypeDropdown" class="col-sm-2 col-form-label">Payment Method</h5>
            <div class="col-sm-10 input-group">
                <div id="paymentTypeDropdown" class="dropdown">
                    <button class="btn btn-primary dropdown-toggle" type="button" id="paymentTypeButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                        @Model.PaymentType.Name
                    </button>

                    <div class="dropdown-menu paymentType" aria-labelledby="paymentTypeButton">
                    @{
                        foreach (var paymentType in Model.PaymentTypes)
                        {
                            Output.Write("<a class=\"dropdown-item paymentType\" value=\"{1}\">{0}</a>", paymentType.Name, paymentType.Id);
                        }
                    }
                    </div>
                </div>
                <input asp-for="PaymentTypeId" id="paymentTypeDropdownInput" type="hidden" />
            </div>
        </div>

        <div class="form-group row">
            <h5 class="col-sm-2 col-form-label">Currency</h5>
            <div class="col-sm-10 input-group">
                <div id="fiatCurrencyDropdown" class="dropdown">
                    <button class="btn btn-primary dropdown-toggle" type="button" id="fiatCurrencyButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                        @Model.FiatCurrency.Name (@Model.FiatCurrency.PairChar)
                    </button>

                    <div class="dropdown-menu fiatCurrency" aria-labelledby="fiatCurrencyButton">
                        @{
                            if (Model.FiatCurrencies != null)
                            {
                                foreach (var fiatCurrency in Model.FiatCurrencies)
                                {
                                    Output.Write("<a class=\"dropdown-item fiatCurrency\" value=\"{1}\">{0} ({2})</a>", fiatCurrency.Name, fiatCurrency.Id, fiatCurrency.PairChar);
                                }
                            }
                        }
                    </div>
                </div>
                <input asp-for="FiatCurrencyId" id="fiatCurrencyDropdownInput" type="hidden" />
            </div>
        </div>

        <hr class="my-3" />

        <div class="form-group row">
            <h5 class="col-sm-2 col-form-label">Margin</h5>
            <div class="col-sm-2">
                <div class="input-group">
                    <input asp-for="Margin" type="number" class="form-control" placeholder="0">
                    <div class="input-group-append">
                      <span class="input-group-text" id="MarginPctAppend">%</span>
                    </div>
                </div>
            </div>
            <!--<div class="col-sm-3">
                <div class="form-check form-check-inline checkbox">
                  <label asp-for="EquationMode" class="form-check-label">
                    <input asp-for="EquationMode" class="form-check-input" type="checkbox" id="eqnModeCheckBox"> Equation Mode (This will replace margin with an equation entry)
                  </label>
                </div>
            </div>-->
        </div>

        <div class="form-group row">
            <h5 class="col-sm-2 col-form-label">Restrict amounts to</h5>
            <div class="col-sm-2 form-inline form-group">
                <input asp-for="AmountRestriction" type="number" class="form-control" placeholder="0">
                <small id="passwordHelpInline" class="text-muted">
                  Optional. Restrict trading amounts to specific denominations.
                </small>
            </div>
        </div>

        <div class="form-group row">
            <h5 class="col-sm-2 col-form-label">Transaction Limits</h5>
                <div class="input-group col-sm-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text" id="MinTxnLimitPrepend">Minimum</span>
                    </div>
                    <input asp-for="MinTxnLimit" type="number" class="form-control" placeholder="0" aria-describedby="MinTxnLimitPrepend">
                </div>
                <div class="input-group col-sm-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text" id="MaxTxnLimitPrepend">Maximum</span>
                    </div>
                    <input asp-for="MaxTxnLimit" type="number" class="form-control" placeholder="1000" aria-describedby="MaxTxnLimitPrepend">
                </div>
        </div>

        <hr class="my-3" />

        <div class="form-group">

            <div class="form-check">
              <label asp-for="AllowOnlyIdenfitiedUser" class="form-check-label">
                <input asp-for="AllowOnlyIdenfitiedUser" class="form-check-input" type="checkbox">Require Identity Verification
              </label>
            </div>

            <div class="form-check">
              <label asp-for="AllowOnlyPhoneVerifiedUser" class="form-check-label">
                <input asp-for="AllowOnlyPhoneVerifiedUser" class="form-check-input" type="checkbox">SMS Verification Required
              </label>
            </div>

            <div class="form-check">
              <label asp-for="AllowOnlyTrustedUser" class="form-check-label">
                <input asp-for="AllowOnlyTrustedUser" class="form-check-input" type="checkbox">Allow only trusted people
              </label>
            </div>

            <div class="form-check">
              <label asp-for="AllowOnlyTradeAfterIdentifyingUser" class="form-check-label">
                <input asp-for="AllowOnlyTradeAfterIdentifyingUser" class="form-check-input" type="checkbox">Allow trades only after going through your validation process
              </label>
            </div>

            <div class="form-check">
              <label asp-for="AllowOnlyWithRealName" class="form-check-label">
                <input asp-for="AllowOnlyWithRealName" class="form-check-input" type="checkbox">Allow only people with their real names
              </label>
            </div>

        </div>

        <hr class="my-3" />

        <div class="form-group row">
            <h5 class="col-sm-2 col-form-label">Terms of Advert</h5>
            <div class="col-sm-7">
                <textarea asp-for="TermsAndCondition" class="form-control" rows="7">@Model.TermsAndCondition</textarea>
            </div>
        </div>

        <input asp-for="Id" type="hidden">
        <!-- https://stackoverflow.com/questions/8054165/using-put-method-in-html-form -->
        <input type="hidden" name="_METHOD" value="PUT"/>

        <button type="submit" class="btn btn-primary">Update advert</button>
    </form>
Nicholas
  • 1,883
  • 21
  • 39
  • Why do you assume the linked question is relevant? The stack trace shows `Executed action CounterCore.Controllers.AdvertController.Modify` which probably means the action executed and MVC choked on the *response*. Did you try debugging? Is the action called? Where *is* the code? What does the action return? – Panagiotis Kanavos Jan 19 '18 at 11:08
  • 2
    Instead of trying things at random I'd suggest you create a *new* project using the basic MVC template and add a simple form, the same way that's shown in all tutorials. Once you know what the working code looks like you'll be able to modify your own project – Panagiotis Kanavos Jan 19 '18 at 11:10
  • @PanagiotisKanavos Step debugging doesn't work, it doesn't even hit the API even though it says the action was executed. The linked question is not relevant directly but is one of the solutions I have attempted. – Nicholas Jan 19 '18 at 11:11
  • if you're not even hitting the API, it would be very relevant to show us how you're trying to call the API (i.e. show us the code of the client which makes the request) – ADyson Jan 19 '18 at 11:15
  • @ADyson Sure, added. It is a bad practice for me to assume the uniformity of Razor views so I apologise. – Nicholas Jan 19 '18 at 11:17
  • have you watched your network tab to be sure it's definitely posting to the correct URL? – ADyson Jan 19 '18 at 11:37
  • Also why have you got `` in there? You're trying to do a POST. I don't know if this will actually have any effect, but it certainly seems unnecessary. – ADyson Jan 19 '18 at 11:38
  • @ADyson apologies, that is commented out on the latest change, I think I accidentally uncommented that while copy over... Alls good Request URL:https://localhost:5001/Advert/Modify/2 Request Method:POST Status Code:200 OK Remote Address:127.0.0.1:5001 Referrer Policy:no-referrer-when-downgrade – Nicholas Jan 19 '18 at 11:40
  • @ADyson That _METHOD entry is for a future implementation for DELETE and PUT – Nicholas Jan 19 '18 at 11:40
  • Status Code:200 OK suggests you did get a response. What is contained within the response? – ADyson Jan 19 '18 at 11:41
  • @ADyson Nothing!!! haha Content-Length:0 Date:Fri, 19 Jan 2018 11:41:27 GMT Server:Kestrel – Nicholas Jan 19 '18 at 11:42
  • @ADyson I'm just confused why I get a one liner error with no stacktrace and a response OK from the client side. Since day 1 using .NET Core's RC and beta days, never faced such a problem – Nicholas Jan 19 '18 at 11:42
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/163504/discussion-between-nicholas-and-adyson). – Nicholas Jan 19 '18 at 16:41

1 Answers1

0

Apologies, if you do notice, I've been saying it has been breaking in-between the request and the API, which it is.

I realised that I have a middleware designed to intercept every request even if it unsuccessful to have a perfect audit trail.

Turns out the error is ViewModel related.

If you ever need an interceptor, here it is

class HttpRequestMiddleware
    {
        /// <summary>
        /// The list of sensitive HTTP form data that we shouldnt log to the server
        /// </summary>
        private static List<string> FilterSensitiveHttpFormData = new List<string>();

        /// <summary>
        /// The list of sensitive HTTP request data that we shouldnt log to the server
        /// </summary>
        private static List<string> FilterSensitiveHttpRequestData = new List<string>();

        private const string MessageTemplate = "[IP: {IpAddr}] requested HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
        private const string ErrorMessageTemplate = "[IP: {0}] requested HTTP {1} {2} error in {3} ms" +
            "\r\nRequest Headers: {4}\r\nRequest Hosts: {5}\r\nRequest Protocol: {6}\r\nRequest Form: {7}\r\n{8}";

        // Logger
        private readonly ILogger<HttpRequestMiddleware> _logger;

        // Etc
        private readonly RequestDelegate _next;


        public HttpRequestMiddleware(RequestDelegate next, ILogger<HttpRequestMiddleware> _logger)
        {
            if (next == null) throw new ArgumentNullException(nameof(next));

            this._next = next;
            this._logger = _logger;

            // Add more here
            if (FilterSensitiveHttpFormData.Count == 0)
            {
                FilterSensitiveHttpFormData.Add("password");
                FilterSensitiveHttpFormData.Add("Password");
                FilterSensitiveHttpFormData.Add("g-recaptcha-response");

                FilterSensitiveHttpRequestData.Add("Cookie");
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public async Task Invoke(HttpContext httpContext, UserManager<User> userManager)
        {
            if (httpContext == null)
                throw new ArgumentNullException(nameof(httpContext));


            string RealIPAddress = (string)httpContext.Items["RealIPAddress"]; // This must be present. Its set via CloudflareIPAuthenticateionMiddleware

            // Query user
               var user = await userManager.GetUserAsync(httpContext.User);
            long UserId = user == null ? 0 : user.Id;

            // Time
            var start = Stopwatch.GetTimestamp();
            try
            {
                await _next(httpContext);

                if (httpContext.Response != null)
                {
                    var elapsedMs = GetElapsedMilliseconds(start, Stopwatch.GetTimestamp());

                    var statusCode = httpContext.Response.StatusCode;

                    AddLogToDatabase(UserId, RealIPAddress, httpContext, GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()), statusCode, null);
                }
            }
            // Never caught, because `LogException()` returns false.
            catch (Exception ex)
            {
                AddLogToDatabase(UserId, RealIPAddress, httpContext, GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()), 0, ex);
            }
        }

        private void AddLogToDatabase(long UserId, string RealIPAddress, HttpContext httpContext, double elapsedMs, int HttpStatusCode, Exception ex)
        {
            var request = httpContext.Request;

            string ExceptionLog = ex == null ? null : ex.ToString();
            string HttpRequestForm = request.HasFormContentType ? string.Join("; ", request.Form.ToDictionary(v => v.Key, v => v.Value.ToString())
                .Where(x => !FilterSensitiveHttpFormData.Contains(x.Key))
                .Select(
                    p => string.Format(
                    "{0}: {1}"
                    , p.Key, p.Value
                ))) : string.Empty;
            string HttpRequestHeader = string.Join("; ", request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString())
                .Where(x => !FilterSensitiveHttpRequestData.Contains(x.Key))
                .Select(
                    p => string.Format(
                    "{0}: {1}"
                    , p.Key, p.Value
                )));
            string HttpRequestHost = request.Host.ToString();
            string HttpRequestMethod = httpContext.Request.Method;
            string HttpRequestProtocol = request.Protocol;
            string HttpRequestPath = httpContext.Request.Path;


            // Add to cache log to be scheduled into database
            StartupConfigurations.LoggingCache.AddMiddlewareLog(new Models.LoggingViewModel.MiddlewareAccessLog()
            {
                ElapsedTime = elapsedMs,
                ExceptionLogs = ExceptionLog,
                HttpRequestForm = HttpRequestForm,
                HttpRequestHeader = HttpRequestHeader,
                HttpRequestHost = HttpRequestHost,
                RequestMethod = HttpRequestMethod,
                HttpRequestProtocol = HttpRequestProtocol,
                HttpStatusCode = HttpStatusCode,
                SessionIPAddress = RealIPAddress,
                RequestPath = HttpRequestPath,
                Timestamp = DateTime.Now,
                UserId = UserId,
            });
        }

        private static double GetElapsedMilliseconds(long start, long stop)
        {
            return (stop - start) * 1000 / (double)Stopwatch.Frequency;
        }
    }

Same as any middleware, Startup.cs right above UseMvc

app.UseMiddleware<HttpRequestMiddleware>(); 

Make sure you have Console.Writeline in the class to output the error in verbose. Some code will have to be refactored due to the implementation of a custom Cloudflare functionality we've got.

Nicholas
  • 1,883
  • 21
  • 39