0

I'm working on a .NET Framework 4.8 project using Web Forms (the one with .aspx pages). During development, I used a WebMethod tasked with fetching and validating data, returning either a success (status code 200) or an error (status code 400) response.

During development, everything worked as expected: it returned the result on success, and when there was a user-related error, it returned a message with guidance that was then displayed in a screen popup to inform the user (this message could be any of numerous validation-related issues).

The problem started after I deployed to production: now, when a 4XX error occurs, it simply returns "Bad Request," and I'm not sure why.

My web method looks like this (some parts are edited for security reasons):

[WebMethod]
public static string List(String codeConstruction, String code, String searchText, String from, String to, String const)
{
    DateTime dateFrom = nUtil.RetornaDataAntiga();
    DateTime dateTo = nUtil.RetornaDataFutura();
    var custActive = const == "1";

    if (!String.IsNullOrEmpty(de))
    {
        try
        {
            dateFrom = DateTime.Parse(de);
        }
        catch (Exception)
        {
            dateFrom = nUtil.RetornaDataAntiga();
        }
    }

    if (!String.IsNullOrEmpty(ate))
    {
        try
        {
            dateTo = DateTime.Parse(ate);
        }
        catch (Exception)
        {
            dateTo = nUtil.RetornaDataFutura();
        }
    }

    if (!custActive)
    {
        var dayFilter = 365;

        if ((String.IsNullOrEmpty(code))
            && (String.IsNullOrEmpty(codeConstruction))
            && (String.IsNullOrEmpty(searchText))
            && ((dateTo - dateFrom).TotalDays > dayFilter))
        {
            return nUtil.RespostaJson(new { mensagem = "Not Enough Filters" }, 400);
        }
    }

    List<eHistoricoCompras> lst = new List<eHistoricoCompras>();

    using (var db = new dbModule())
    {
        lst = db.ListThings()
    }

    return nUtil.RespostaJson(lst);
}

This is the method that creates the response:

public static String RespostaJson<T>(T objeto, int statusCode = 200)
{
    if (statusCode == 0)
    {
        statusCode = 500;
    }

    HttpContext.Current.Response.StatusCode = statusCode;

    return JsonConvert.SerializeObject(objeto);
}

Frontend AJAX Call And my call is like this:

JavaScript

$.ajax({
    type: "POST",
    url: url,
    contentType: 'application/json; charset=utf-8',
    data: options.body ? JSON.stringify(options.body) : null,
    dataType: 'json',
    success: (data, status, xhr) => {

        if (!data.d) {
            response = { data: "", error: null, status: xhr.status };
        } else {
            const obj = JSON.parse(data.d);
            response = { data: obj, error: null, status: xhr.status };
        }

        if (options.popupOnSuccess) {
            showFastAlertWithStatus(response);
        }
        return resolve(response);
    },
    error: (xhr, status, error) => {
        if (xhr.status > 499) {
            response = { data: null, error: xhr.responseJSON, status: xhr.status };
        }
        else {
            if (!xhr.responseJSON) {
                response = { data: null, error: { mensagem: status }, status: xhr.status === 200 ? 500 : xhr.status };
            }
            else if (!xhr.responseJSON?.d) {
                let errorObj = xhr.responseJSON;
                if (!xhr.responseJSON?.mensagem && xhr.responseJSON?.Message) {
                    errorObj = { mensagem: xhr.responseJSON?.Message }
                }

                response = { data: null, error: errorObj, status: xhr.status };
            }
            else {
                const obj = JSON.parse(xhr.responseJSON.d);
                response = { data: null, error: obj, status: xhr.status };
            }
        }

        if (options.popupOnError) {
            showFastAlertWithStatus(response);
        }

        return resolve(response);
    }
});

I tried checking if it could be a web.config setting in production that wasn't in my development environment, but both already had the same structure.

The response I get (happens in production):

enter image description here

The response I want (happens in development):

enter image description here

1
  • Does the web method call EVER work? When you deploy, then often the path name used may well change somewhat. (especially if the publish is a sub site or sub folder and not root). And you don't show how you setting up the var URL - and I would check this. So, I would place a "alert" or at the very least a consolog.log(URL) in your client side code to allow debugging of that variable. As noted, do check if URL var is as expected if non of the web method calls work..... Commented Aug 23 at 0:18

2 Answers 2

0

I would look towards how you're setting the URL for the jQuery .ajax call. You don't show how that URL is being setup. However, often with deployment, your production site might be down a folder or so on the web server (or up a folder or so).

And of course if you using a master + child page, then again, the final URL will be that of the location of the child page, not the master page. As a result, I tend to find using a server side expression for the jQuery.ajax call solves this issue.

So, jQuery ajax calls are quite sensitive to the correct URL, so I often use code like this:

            $.ajax({
                type: "POST",
                url: '<%= ResolveUrl("~/PDF.aspx/GetRowVer") %>',
                data: JSON.stringify({ PCHID : PCHID, strPortalComp : HPortal }),
                contentType: "application/json; charset=utf-8",

Note closely that "~" (for path from base root of the site) is NOT available client side (jQuery/JavaScript), but it certainly is from a server side expression. And it can also be used in controls that have runat="server".

To make matters worse, I often use/have such .ajax calls in some of my user controls, and thus once again, I can't really ever know the current path name, since I have quite a few web pages nested in folders, and they have instances of my user controls.

The above server expression thus can be used to correctly (and always) to correctly resolve the URL I want for the jQuery ajax call.

You not provided how you set up the URL (and I understand security concerns posting as such in a public forum), but do consider the above syntax and server expression.

Sign up to request clarification or add additional context in comments.

1 Comment

Hello Albert, the request URL is correct; it works perfectly in development. What's happening is that after validation, I'm returning a 4xx status code, which causes the response you're seeing. When I make the correct request with all fields properly filled, it returns the information correctly. That's why I suspect the status code is the issue.
0

I found the answer. It wasn't a problem with the application itself, but rather an IIS configuration issue. That's why it worked in development but not in production. What was happening is that by default, IIS automatically handles HTTP errors (any 4XX or 5XX error), which caused it to overwrite the error object I was sending.

What I did was add the following to the system.webServer tag in my web.config:

XML

<httpErrors errorMode="Custom" existingResponse="PassThrough">
</httpErrors>

However, be careful when adding this to your application. This will cause any errors from now on to pass through IIS without treatment. This means exceptions could be sent to the client, potentially revealing how your application works, similar to the stack traces returned in development when an error occurs, instead of the old "Bad Request" text. To mitigate this, you should create a Module inheriting from IHttpModule that captures errors coming from the server. Evaluate the error type, and if it's an unexpected error, handle it by returning a custom response like this:

JSON

{
    "message": "An unexpected error occurred. \r\nSystem administrators have been notified. Error ID: 106025",
    "errorLogId": 106025
}

It captures the server's response after any type of status code.

C#

using System;
using System.Web;

public class WebMethodErrorModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.PreSendRequestHeaders += new EventHandler(OnPreSendRequestHeaders);
        context.PostMapRequestHandler += new EventHandler(OnPostMapRequestHandler);
    }

    private void OnPostMapRequestHandler(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;
        HttpContext context = app.Context;

        // Check if it's a request to a Page Method (WebMethod)
        // This heuristic might need to be more robust depending on your URLs
        if (context.Handler is System.Web.UI.Page && !string.IsNullOrEmpty(context.Request.PathInfo))
        {
            // For WebMethods, the Content-Type is usually application/json
            string contentType = context.Request.ContentType?.Split(';')[0];
            if (string.Equals(contentType, "application/json", StringComparison.OrdinalIgnoreCase))
            {
                // This is where we hook our custom response filter
                app.Response.Filter = new WebMethodErrorLoggerStream(app.Request, app.Response, app.Response.Filter);
            }
        }
    }

    private void OnPreSendRequestHeaders(object sender, EventArgs e)
    {
        // This event can be used if you want to inspect headers or modify them
        // before the response is sent. However, for logging the body content,
        // the Response.Filter approach is generally better.
    }

    public void Dispose() { }
}

This will use the response that would be sent to the server. In my case, if it's any type of 4XX error, I don't want to do anything in the Close() method.

C#

using Ent; // Assuming these are your custom entity/business logic namespaces
using Neg; // Assuming these are your custom entity/business logic namespaces
using System;
using System.Configuration;
using System.IO;
using System.Text;
using System.Web;
using System.Web.Script.Serialization;

public sealed class WebMethodErrorLoggerStream : Stream
{
    private readonly HttpResponse _response;
    private readonly HttpRequest _request;
    private readonly Stream _baseStream;
    private readonly MemoryStream _capturedStream = new MemoryStream();

    public WebMethodErrorLoggerStream(HttpRequest request, HttpResponse response, Stream baseStream)
    {
        _request = request;
        _response = response;
        _baseStream = baseStream;
    }

    public override bool CanRead => _baseStream.CanRead;
    public override bool CanSeek => _baseStream.CanSeek;
    public override bool CanWrite => _baseStream.CanWrite;
    public override long Length => _baseStream.Length;

    public override long Position
    {
        get => _baseStream.Position;
        set => _baseStream.Position = value;
    }

    public override void Flush()
    {
        _baseStream.Flush();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        return _baseStream.Read(buffer, offset, count);
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        return _baseStream.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
        _baseStream.SetLength(value);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        // Write to our internal buffer first
        _capturedStream.Write(buffer, offset, count);
        // Also write to the original stream
        _baseStream.Write(buffer, offset, count);
    }

    public override void Close()
    {
        // This is where you process the captured content
        _capturedStream.Position = 0;
        using (StreamReader reader = new StreamReader(_capturedStream, Encoding.UTF8))
        {
            if (_response.StatusCode < 500)
            {
                base.Close();
                return;
            }

            string responseContent = reader.ReadToEnd();

            // Attempt to parse the response as a .NET exception serialized to JSON
            // The structure is typically: {"Message":"Error details","StackTrace":"..."}
            try
            {
                var serializer = new JavaScriptSerializer();
                var obj = serializer.Deserialize<dynamic>(responseContent);

                if (obj != null && obj.ContainsKey("Message") && obj.ContainsKey("StackTrace"))
                {
                    // This is likely an unhandled exception from a WebMethod
                    string errorMessage = obj["Message"];
                    string stackTrace = obj["StackTrace"];

                    int companyId = nUtil.RequestHeaderEmpresa(_request); // Assuming nUtil.RequestHeaderEmpresa gets company ID
                    int personId = nUtil.RequestHeaderIdPessoa(_request); // Assuming nUtil.RequestHeaderIdPessoa gets person ID
                    string language = nUtil.RequestHeaderIdioma(_request); // Assuming nUtil.RequestHeaderIdioma gets language


                    string user = "User not found";
                    if (personId == 0)
                    {
                        user = $"userId: {personId}";
                    }

                    // Log this error globally
                    Int32 errorLogId = LogError(user, errorMessage, stackTrace, "Unhandled error", companyId); // Assuming LogError is a global logging function

                    _response.Clear(); // Clear the existing error response
                    if (_response.StatusCode == 0)
                    {
                        _response.StatusCode = 500;
                    }

                    _response.Write(serializer.Serialize(new { message = $"An unexpected error occurred. \r\nSystem administrators have been notified. Error ID: {errorLogId}", errorLogId = errorLogId }));

                    _response.ContentType = "application/json";
                }
            }
            catch (Exception ex)
            {
                // Not a serialized .NET exception, or couldn't parse.
                // You might still log the raw content if it's an unexpected format.
                System.Diagnostics.Debug.WriteLine($"Failed to parse WebMethod error response: {responseContent}. Error: {ex.Message}");
            }
        }
        base.Close();
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.