I have an ASP.NET Core 8.0 Web API created with C# that provides an endpoint to create PDFs from HTML markup. I use PuppeteerSharp to create the PDF, and it worked fine locally, but it does not work when deployed to IIS on a Windows Server machine.
To investigate this issue further, I have extracted the PDF creation code into a standalone application:
using PuppeteerSharp;
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace MSS.PdfExport
{
public class Parameters
{
public string Html { get; set; }
public string Base64 { get; set; }
public Parameters()
{
Html = string.Empty;
Base64 = string.Empty;
}
}
internal class Program
{
static void Main(string[] args)
{
var streamWriter = new StreamWriter(@"C:\Temp\Test\Log.txt", false, Encoding.UTF8)
{
AutoFlush = true
};
// Read parsed HTML code.
var stringBuilder = new StringBuilder();
while (true)
{
var ch = Convert.ToChar(Console.Read());
stringBuilder.Append(ch);
if (ch == '\n')
{
break;
}
}
var sbText = stringBuilder.ToString();
streamWriter.WriteLine($"{nameof(stringBuilder)} = {stringBuilder}");
streamWriter.WriteLine();
streamWriter.WriteLine();
streamWriter.WriteLine();
// Get HTML code.
var html = WebUtility.UrlDecode(sbText);
streamWriter.WriteLine($"{nameof(html)} = {html}");
streamWriter.WriteLine();
streamWriter.WriteLine();
streamWriter.WriteLine();
// Execute generator.
try
{
var test = new Parameters()
{
Html = html
};
ExportPdf(test).Wait();
Console.WriteLine(test.Base64);
File.WriteAllText(@"C:\Temp\Test\output.txt", test.Base64);
}
catch (Exception ex)
{
streamWriter.WriteLine();
streamWriter.WriteLine();
streamWriter.WriteLine();
streamWriter.WriteLine($"{nameof(ex)} = {ex}");
}
finally
{
streamWriter.Close();
}
}
private static async Task ExportPdf(Parameters parameters)
{
var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
Args = ["--no-sandbox", "--disable-setuid-sandbox"]
});
using (var page = await browser.NewPageAsync())
{
await page.SetContentAsync(parameters.Html);
var data = await page.PdfDataAsync(new PdfOptions
{
PrintBackground = true
});
parameters.Base64 = Convert.ToBase64String(data);
}
await browser.CloseAsync();
}
}
}
The application reads a URL-encoded HTML input stream, converts it to HTML, and finally uses Puppeteer to open a headless Chromium to convert the HTML into PDF and write the results in console and write into a file.
If I save the URL-encoded HTML text into a text file and execute my application via Windows command line, it works perfectly fine!
MSS.PdfExport.exe < "C:\Temp\Test\New Text Document.txt"
However, when I call the application from my API with this code:
// Decode the HTML code.
var html = WebUtility.UrlDecode(model.Html);
_logger.LogInformation($"{this}.Index > Execute generation.");
var htmlBytes = Encoding.UTF8.GetBytes(model.Html);
var utf8Html = Encoding.UTF8.GetString(htmlBytes);
_logger.LogInformation($"{this}.Index > {nameof(utf8Html)} = {utf8Html}");
_logger.LogInformation($"{this}.Index > {nameof(utf8Html.Length)} = {utf8Html.Length}");
var startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = @"C:\Temp\PdfExport\MSS.PdfExport.exe";
startInfo.WorkingDirectory = @"C:\Temp\PdfExport\";
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardInput = true;
var output = string.Empty;
try
{
using (var process = Process.Start(startInfo))
{
_logger.LogInformation($"{this}.Index > Process started. {nameof(startInfo)} = [{startInfo}]");
if (process is not null)
{
using (var writer = process.StandardInput)
{
if (writer.BaseStream.CanWrite)
{
writer.WriteLine(utf8Html);
}
}
var reader = process.StandardOutput;
output = reader.ReadToEnd();
_logger.LogInformation($"{this}.Index > {nameof(output)} = \"{output}\"");
process.WaitForExit();
}
}
}
catch (Exception exception)
{
_logger.LogInformation($"{this}.Index > {nameof(exception)} = {exception}");
}
Puppeteer does not work and I get the following exception:
System.AggregateException: One or more errors occurred. (Failed to launch browser!
[112204:131028:1023/144647.887:ERROR:base\win\message_window.cc:124] Failed to register the window class for a message-only window: The requested lookup key was not found in any active activation context. (0x36B7))
PuppeteerSharp.ProcessException: Failed to launch browser! [112204:131028:1023/144647.887:ERROR:base\win\message_window.cc:124] Failed to register the window class for a message-only window: The requested lookup key was not found in any active activation context. (0x36B7)
at PuppeteerSharp.States.ProcessStartingState.StartCoreAsync(LauncherBase p) in /home/runner/work/puppeteer-sharp/puppeteer-sharp/lib/PuppeteerSharp/States/ProcessStartingState.cs:line 83
at PuppeteerSharp.States.ProcessStartingState.StartCoreAsync(LauncherBase p) in /home/runner/work/puppeteer-sharp/puppeteer-sharp/lib/PuppeteerSharp/States/ProcessStartingState.cs:line 89
at PuppeteerSharp.Launcher.LaunchAsync(LaunchOptions options) in /home/runner/work/puppeteer-sharp/puppeteer-sharp/lib/PuppeteerSharp/Launcher.cs:line 77
at PuppeteerSharp.Launcher.LaunchAsync(LaunchOptions options) in /home/runner/work/puppeteer-sharp/puppeteer-sharp/lib/PuppeteerSharp/Launcher.cs:line 110
at MSS.PdfExport.Program.ExportPdf(Parameters parameters) in [redacted]\MSS.PdfExport\Program.cs:line 94
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.Tasks.Task.Wait()
at MSS.PdfExport.Program.Main(String[] args) in [redacted]\MSS.PdfExport\Program.cs:line 69