1

The Setup

We have a .net web api rest service that basically will render a web page as an image and attach that image to an email and send it to an address. We pass the service a JSON email object that contains the from, to, subject, body, mailServerName, and html to render. This service will be called a a lot, however, its the first call that gives the problem.

The problem

The first .net web api rest service request of the day always throws the following exception:

Message: "An error has occurred.", ExceptionMessage: "Object Reference not set to an instance of an object", ExceptionType: "System.NullReferenceException", Stacktrace: "at
EmailService.Controllers.EmailController.Post(Email email) at
lambda_method(Closure, Object, Object[]) at ...

Note:

After adding the custom exceptions below, the exception message is now the following:

ExceptionMessage: "Exception happened while saving Image to Jpeg stream"

Stacktrace:

at EmailService.Controllers.EmailController.Post(Email email) at
lambda_method(Closure, Object, Object[]) at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c_DisplayClass10.b_9(Object instance,Object[] methodParameters) at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(HttpControllerContext controllerContext,IDictionary`2 arguments, CancellationToken cancellationToken) at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Tasktask) at System.RuntimeCompilerServices.TaskAwaiter.HandleNonSuccessAnsDebuggerNotification(Task task)....

which is related to this line of code:

websiteImage.Save(websiteStream, System.Drawing.Imaging.ImageFormat.Jpeg);

The service works fine after the first request in performed. Again, its only after a long wait, typically until the next day, do we see the above exception thrown.

The Question

What is causing the service to throw the NullReferenceException and how can I fix it?

The C# code for the controller class and the jQuery that performs the actual requst are below.

Controller class:

namespace EmailService.Controllers
{

    public class EmailController : ApiController
    {

        // POST api/email
        public HttpResponseMessage Post(Email email)
        {

            if (email == null)
            {
                throw new Exception("Email parameter is null");
            }

            Bitmap websiteImage;

            try
            {
                websiteImage = WebsiteToImage.Generate();
            }

            catch (Exception)
            {

                // 500 Internal Server Error
                response.StatusCode = HttpStatusCode.InternalServerError;

                throw new Exception("Exception happened while generating Image");
            }



            // create memory stream from bitmap and save it as a jpeg, this allows us to attach the image from memory, without having to store it on the server
            System.IO.Stream websiteStream = new System.IO.MemoryStream();
            try
            {
                websiteImage.Save(websiteStream, System.Drawing.Imaging.ImageFormat.Jpeg);
            }


            catch (Exception)
            {

                // 500 Internal Server Error
                response.StatusCode = HttpStatusCode.InternalServerError;

                throw new Exception("Exception happened while saving Image to Jpeg stream");
            }


            try
            {

                websiteStream.Position = 0;
            }


            catch (Exception)
            {

                // 500 Internal Server Error
                response.StatusCode = HttpStatusCode.InternalServerError;

                throw new Exception("Exception happened while setting stream posiiton=0");
            }


            // create response with status code 200 OK, since we aren't actually creating anything 
            var response = this.Request.CreateResponse(HttpStatusCode.OK);


            try
            {
                // MailMessage is used to represent the e-mail being sent
                using (MailMessage message = new MailMessage(email.from, email.to, email.subject, email.body))
                {

                    // attach jpeg from memory
                    message.Attachments.Add(new Attachment(websiteStream, "letter.jpg", "image/jpeg"));

                    // create mail client
                    SmtpClient mailClient = new SmtpClient(email.mailServerName);

                    // use the Windows credentials of the account (i.e. user account) 
                    mailClient.UseDefaultCredentials = true;

                    // send the message
                    mailClient.Send(message);
                }

            }
            catch (Exception)
            {

                // 500 Internal Server Error
                response.StatusCode = HttpStatusCode.InternalServerError;

                throw new Exception("Exception happened while creating and sending mail message");

            }

            //return new HttpResponseMessage() { Content = new StringContent(html) };
            return response;

        }

        // PUT api/email/5
        public HttpResponseMessage Put(int id, [FromBody]string value)
        {
            // return response status code of 501 Not Implemented
            return this.Request.CreateResponse(HttpStatusCode.NotImplemented);
        }

    }
}

Service request code:

$.ajax ({
        type: "POST",
        url: "/api/email/",
        contentType: "application/json; charset=utf-8",
        data: JSON.stringify(email),
        success: finish,
        error: error

    });

Global.asax

   public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {

            // add details to server errors 
            GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;

            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);


        }
    }

WebApiConfig.cs

 public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }

WebsiteToImage.cs

public class WebsiteToImage
{
    private Bitmap m_Bitmap;
    private string m_Url;
    private string m_FileName = string.Empty;

    private string m_html;

    public WebsiteToImage(string html)
    {
        // Without file 
        //m_Url = url;

        m_html = html;

    }

    public WebsiteToImage(string url, string fileName)
    {
        // With file 
        m_Url = url;
        m_FileName = fileName;
    }

    public Bitmap Generate()
    {
        // Thread 
        var m_thread = new Thread(_Generate);
        m_thread.SetApartmentState(ApartmentState.STA);
        m_thread.Start();
        m_thread.Join();
        return m_Bitmap;
    }

    private void _Generate()
    {
        WebBrowser browser = new WebBrowser { ScrollBarsEnabled = false };
        //browser.Navigate(m_Url);
        browser.DocumentText = m_html;

        browser.DocumentCompleted += WebBrowser_DocumentCompleted;

        while (browser.ReadyState != WebBrowserReadyState.Complete)
        {
            Application.DoEvents();
        }

        browser.Dispose();
    }

    private void WebBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
    {
        // Capture 
        var browser = (WebBrowser)sender;
        browser.ClientSize = new Size(browser.Document.Body.ScrollRectangle.Width, browser.Document.Body.ScrollRectangle.Bottom);
        browser.ScrollBarsEnabled = false;
        m_Bitmap = new Bitmap(browser.Document.Body.ScrollRectangle.Width, browser.Document.Body.ScrollRectangle.Bottom);
        browser.BringToFront();
        browser.DrawToBitmap(m_Bitmap, browser.Bounds);

        // Save as file? 
        if (m_FileName.Length > 0)
        {
            // Save 
            m_Bitmap.SaveJPG100(m_FileName);
        }
    }
}

public static class BitmapExtensions
{
    public static void SaveJPG100(this Bitmap bmp, string filename)
    {
        var encoderParameters = new EncoderParameters(1);
        encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L);
        bmp.Save(filename, GetEncoder(ImageFormat.Jpeg), encoderParameters);
    }

    public static void SaveJPG100(this Bitmap bmp, Stream stream)
    {
        var encoderParameters = new EncoderParameters(1);
        encoderParameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L);
        bmp.Save(stream, GetEncoder(ImageFormat.Jpeg), encoderParameters);
    }

    public static ImageCodecInfo GetEncoder(ImageFormat format)
    {
        var codecs = ImageCodecInfo.GetImageDecoders();

        foreach (var codec in codecs)
        {
            if (codec.FormatID == format.Guid)
            {
                return codec;
            }
        }

        // Return 
        return null;
    }
}

Email.cs

namespace EmailService.Models
{
    public class Email
    {
        // email properties
        public string mailServerName {get; set;}
        public string from {get; set;}
        public string to {get; set;}
        public string subject {get; set;}
        public string body {get; set;}
        public string content { get; set; }

    }
}
Zengineer
  • 508
  • 1
  • 7
  • 19
  • possible duplicate of [What is a NullReferenceException and how do I fix it?](http://stackoverflow.com/questions/4660142/what-is-a-nullreferenceexception-and-how-do-i-fix-it) – Soner Gönül May 09 '14 at 13:53
  • 1
    Do you know at which line does the nullref gets thrown? You can sprinkle some logging between the lines to see which line is the culprit. It'll give more context to understand what's going on... – LB2 May 09 '14 at 14:49
  • I don't know which line. The exception seems to lead that its at the Post method call. I've put checks throughout the code but they are never hit. Its almost as if the exception is thrown before execution gets inside of the Post method. – Zengineer May 09 '14 at 14:54
  • @Zengineer Exception states that it is inside `Post` so execution does get in. Just not clear which line. What logging infrastructure do you currently have? Can you sprinkle in some logs between lines so you can trace later which messages got logged and which one didn't (thus showing the offending line)? If not, another approach you can put a bunch of `try/catch`-es and wrap exceptions in new exception noting which one it was. Another thing, post definition of `Email` class - I bet some field there that you expect non-null perhaps comes out as null? – LB2 May 09 '14 at 15:13
  • OK, I will add some more `try/catch` blocks to see if I can track it down. The `WebsiteToImage.Generate()` looks to create a thread so that may be something of a cause. I will post the Email class. Another problem is recreating the issue without waiting an entire day to retest. – Zengineer May 09 '14 at 15:18
  • @LB2 The exception is being thrown at this line of code `websiteImage.Save(websiteStream, System.Drawing.Imaging.ImageFormat.Jpeg);` in `EmailController.cs` – Zengineer May 16 '14 at 14:47
  • @Zengineer, if you reset iis (obviously not on production) does the problem happen again on the first try? I would imagine that it would. By default IIS is recycled I believe after 20 minutes of inactivity so I suspect that is why you see it each morning. – MDiesel May 16 '14 at 15:08
  • @Zengineer, can you try creating the bitmap without calling the Generate method (which is doing the threading)? I suspect that this is the issue – MDiesel May 16 '14 at 15:09
  • @MDiesel I will comment out Generate() for testing purposes, but it is needed for production. Is there a way to remove threading from the Generate method? I didn't write it and have little history with C#/.NET so I'm not sure what my options are. – Zengineer May 16 '14 at 15:31
  • @MDiesel Also, I removed the regular time intervals to recycle the Application Pool for the service. I thought that would solve the problem. Some mornings we do not receive the error message, but this morning we did. – Zengineer May 16 '14 at 15:34
  • @Zengineer, the only line you would need in the generate is: public Bitmap Generate() { // Thread return m_Bitmap; } – MDiesel May 16 '14 at 16:39
  • @Zengineer `_Generate()` is suspect to me. `Generate()` uses thread to call `_Generate()` which creates the resource that later causes `nullref` - can you please post code for `_Generate()` so we could analyze it. My theory is that `_Generate` fails and results in `m_Bitmap` being `null` which later code tries to dereference causing nullref. A wild guess is that there is a lack of meeting some pre-condition, bitmap fails to get created, and thus `m_Bitmap` is `null` causing havoc downstream. But pre-condition is satisfied in later calls. – LB2 May 16 '14 at 17:22
  • @MDiesel See edits for the entire `WebsiteToImage.cs` class. It contains `_Generate()` code. – Zengineer May 16 '14 at 20:42

1 Answers1

0

From everything I have been reading it looks like there are some issues using the Single Threaded ApartmentState (STA) in an ASP.NET application. Take a look at the article here which describes how the author was able to work around these issues.

I have not been able to determine why exactly this behavior is only happening for you on the initial request of the day.

Additional resources for this can be found here and here. I hope this helps!

Try changing your generate method to (assuming that m_Bitmap has been constructed elsewhere in that class):

public Bitmap Generate()
{
    return m_Bitmap;
}
MDiesel
  • 2,619
  • 10
  • 14
  • It doesn't appear to be related to the `Generate()` method, I added try/catch around the calls and it didn't thrown an exception? Also, why would it only fail on the first request? You would think the issue would happen for every request, if it was related to threading, no? – Zengineer May 13 '14 at 14:41
  • The line that is throwing the exception is `websiteImage.Save(websiteStream, System.Drawing.Imaging.ImageFormat.Jpeg);` which is in `EmailController.cs` – Zengineer May 16 '14 at 14:45
  • @Zengineer, this is very strange. Can you provide the stack trace from the caught exception? – MDiesel May 16 '14 at 14:49
  • check my edits for the StackTrace. I believe it's identical to the original exception's stacktrace. – Zengineer May 16 '14 at 15:02
  • If I remove threading from `Bitmap Generate()`, the service throws the same exception at the same line of code on every request. Basically, without threading `m_Bitmap` is always null. – Zengineer May 19 '14 at 20:37
  • @Zengineer, I had first suggested that prior to you posting that whole class. Is the business idea behind that class to take a screenshot? I'm wondering if we could find a different way to accomplish the same. This is a very perplexing issue – MDiesel May 19 '14 at 20:39
  • Yes that is exactly what the biz need is. We need to take screenshot of webpage. We have a workaround in place that will attempt to resend the email if the initial request returns an error. Since this only seems to happen for the first request of the day, we don't expect it to be too much of an issue. – Zengineer May 19 '14 at 20:44
  • @Zengineer, that seems like a feasible workaround, especially since it doesn't happen much. What a crazy issue – MDiesel May 19 '14 at 20:50