ContentType not supported exception

I have an odd issue regarding a route to download PDF files that have chinese characters in their filename

I supply the filename to download as part of the route like this
[Route("/api/v1/downloads/{path*}", Verbs = “GET”)]
The route returns an HttpResult populated by a FileInfo object and parameter attachment = true

Some files throw this exception
ServiceStack.ServiceStackHost - Error occured while Processing Request: ContentType not supported ‘application/pdf’
System.NotSupportedException: ContentType not supported ‘application/pdf’
at ServiceStack.Host.ContentTypes.d__26.MoveNext()

with an HTTP 400

while other PDF files with chinese characters in their name succeed with a HTTP 200

I test the route with a Razor UI we built into the service application (comparable to the swaggerui but with more features)
I do not register the application/pdf contenttype in the apphost (tried that, same result)

It seems that some files are handled differently within servicestack than others, yielding the exception shown
Btw, I had the same issue with image files that have chinese characters in its name so it is not a PDF specific issue

I use servicestack 5.1.0

Can you try with v5.1.1 in MyGet as an issue similar to this was already resolved.

Updated our solution to servicestack 5.1.1 but still the same exception
I see a difference in the response headers

  • a file that fails has the Transfer-Encoding header set to chunked
  • a file that succeeds does not have this header at all (although this file is larger than the failed one)

Please post the raw HTTP Request/Response Headers of the file with the issue along with the service implementation you’re using to return the file.

Request Headers

GET /api/v1/downloads/%7Bpath*%7D?path=localdescription%2F13870%2F47323%2F%E5%B8%83%E5%88%A9%E5%A1%94.pdf&_=1531295086568 HTTP/1.1
Host: localhost:9999
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/javascript, */*; q=0.01
DNT: 1
X-Requested-With: XMLHttpRequest
Authorization: Bearer --token-goes-here--
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Referer: http://localhost:9999/docs/operations
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: ss-pid=eSjiT4TMSv4XHF8uYmGA; ss-id=Y5dGNYkV6v2P5bo6nmv6

Response Headers

HTTP/1.1 400 Bad Request
Transfer-Encoding: chunked
Content-Type: application/pdf
Vary: Accept
Server: Microsoft-HTTPAPI/2.0
X-Powered-By: ServiceStack/5,11 NET45 Win32NT/.NET
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Access-Control-Allow-Headers: Content-Type
Date: Wed, 11 Jul 2018 14:43:35 GMT

NOTE: the preview does not render the Accept header correctly, the last type is * / * (without the extra spaces) but I see only /

// Interfaces
public interface IDownloadResponse : IReturn<IHttpResult>, IReturn
{
}

// Commands
[Route("/api/v1/downloads/{path*}", Verbs = "GET")]
public class TestDownloadCommand : IDownloadResponse
{
    public string Path { get; set; }
}

// Return types
public class DownloadResponse : HttpResult
{
    public DownloadResponse(FileInfo fileToReturn)
        : base(fileToReturn, true)
    {
        // Add some custom headers named DowloadFileName and DownloadFileSize
        Headers.Add(CustomHttpHeaders.DownloadFileName, fileToReturn.Name);
        Headers.Add(CustomHttpHeaders.DownloadFileSize, fileToReturn.Length.ToString());
    }

    public DownloadResponse(DownloadErrorType errorType, string message)
    {
        StatusCode = HttpStatusCode.NotFound;
        StatusDescription = message;

        Headers.Add(CustomHttpHeaders.DownloadErrorType, errorType.ToString());
    }
}

// Service
public DownloadResponse Get(TestDownloadCommand command)
{
    // Construct the full path from the provided partial path
    var path = Path.Combine("c:\\data\\fileattachments", command.Path.Replace("/", "\\"));
    return new DownloadResponse(new FileInfo(path));            
}

The above service code is just the happy flow (it does not test for file existence, in that case we’d return a DownloadResponse with the error message instead of the FileInfo object)

This isn’t the happy path, the Response indicates an Exception was thrown. Can you debug or inspect the Error Handling callbacks to see what Exception was thrown?

Sure. I get these exceptions:

Specified value has invalid Control characters.
Parameter name: value
at System.Net.WebHeaderCollection.CheckBadChars(String name, Boolean isHeaderValue)
at System.Net.WebHeaderCollection.SetInternal(String name, String value)
at ServiceStack.HttpResponseExtensionsInternal.d__7.MoveNext()

and then

ContentType not supported ‘application/pdf’
at ServiceStack.Host.ContentTypes.d__26.MoveNext()
— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ServiceStack.HttpResponseExtensionsInternal.d__14.MoveNext()
— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ServiceStack.HostContext.d__67.MoveNext()

— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ServiceStack.HttpResponseExtensionsInternal.d__8.MoveNext()
— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ServiceStack.HttpResponseExtensionsInternal.d__7.MoveNext()
— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at ServiceStack.HttpResponseExtensionsInternal.d__7.MoveNext()
— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ServiceStack.Host.Handlers.ServiceStackHandlerBase.d__13.MoveNext()
— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ServiceStack.Host.Handlers.ServiceStackHandlerBase.d__12.MoveNext()
— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ServiceStack.Host.RestHandler.d__14.MoveNext()

My guess is that some files we have contain odd characters in their name, as the first exception is thrown by a .NET framework method to set headers called from within ServiceStack.
Putting the filename that is in the FileInfo object in a header value throws this exception, I suppose the header that fails is the Content-Disposition header because it contains the filename and -size
Funny thing is that File.Exists(…) does not complain about these characters

The underlying Exception is from using invalid characters in your HTTP Headers. This is from .NET Framework WebHeaderCollection collection so the only solution would be to avoid using the illegal characters. The illegal character validation isn’t related to validation that File.Exists() uses.

The StackTrace only says that the error occurs when writing the custom HttpResult to the Response it doesn’t indicate that this error is with the Content-Disposition header, it could be any header.

Thanks for the reply
If the filename contains characters invalid to use in an HTTP header, how would I fix that without renaming these files?
I have to pass in a FileInfo object in the HttpResult’s ctor, which checks for file existence (looked that up in the HttpResult class code)

I agree the stacktrace does not show which HTTP header fails, but when I set the asAttachment attribute to false, I get no exception and a normal response without the Content-Disposition header. So I have a strong feeling it is that header that fails

Code from the ServiceStack repo:

public HttpResult(FileInfo fileResponse, string contentType = null, bool asAttachment = false)
        : this(null, contentType ?? MimeTypes.GetMimeType(fileResponse.Name), HttpStatusCode.OK)
    {
        this.FileInfo = fileResponse ?? throw new ArgumentNullException(nameof(fileResponse));
        this.LastModified = fileResponse.LastWriteTime;
        this.AllowsPartialResponse = true;
        if (!FileInfo.Exists)
            throw HttpError.NotFound($"{FileInfo.Name} was not found");

        if (!asAttachment) return;

        var headerValue = $$"attachment; filename=\"{fileResponse.Name}\"; size={fileResponse.Length}; " +
 $$"creation-date={fileResponse.CreationTimeUtc.ToString("R").Replace(",", "")}; " +
 $$"modification-date={fileResponse.LastWriteTimeUtc.ToString("R").Replace(",", "")}; " +
 $$"read-date={fileResponse.LastAccessTimeUtc.ToString("R").Replace(",", "")}";

        this.Headers = new Dictionary<string, string>
        {
            { HttpHeaders.ContentDisposition, headerValue },
        };
    }

If you don’t want to remove the invalid characters from the file name you can try encoding the file name, see this StackOverflow answer for different encoding approaches:

Going to leave the default behavior in HttpResult as using filename*=UTF-8''... has breaking behavior for some clients.

Ok, thanks. I’ll look into that