[tink_web] Remoting: how to properly handle a binary response (i.e. a PDF document)?

Tinkerbell Web’s Remoting is perfect when dealing with a RESTful API that only produces JSON responses. But when an API endpoint produces some PDF document (or other binary content), it’s more complex to handle it…

My remote interface should ideally look like this:

interface RemoteApi {
  @:get('/$id')
  @:produces("application/pdf")
  function getPdfDocument(id: String): js.html.Blob;
}

I’m facing 2 issues…

How to customize the “Accept” request header?
This header is not set, resulting in the browser’s default Accept: */*. How can I set the Accept request header?
@:produces("application/pdf") seems to be ignored, and trying to add @:header("accept", "application/pdf") does not change anything.

How to smoothly handle the server response?
Using js.html.Blob as response type does not work (i.e. “No reader available for mime type application/pdf”). How can I register a new response reader?

Using tink.Chunk, haxe.io.Bytes or String as response type results in the same thing: the server response is tink.http.Response.IncomingResponse instead of the expected type.

So I must write this:

remoteApi.getPdfDocument("ABC").next(IncomingResponse.readAll).handle(outcome -> switch outcome {
	case Failure(_):
		trace("Arg!");
	case Success(chunk):
		final file = new js.html.File([chunk.toBlob()], "MyDocument.pdf", {type: "application/pdf"});
		// Do what I want with this file.
});

But I ideally want to write that:

remoteApi.getPdfDocument("ABC").handle(outcome -> switch outcome {
	case Failure(_):
		trace("Arg!");
	case Success(blob):
		final file = new js.html.File([blob], "MyDocument.pdf", {type: "application/pdf"});
});

If I understand what you’re after, I’d say it’s currently not possible, at least in a straightforward manner.

  1. tink_web has @:produces and @:consumes annotations for routes that produces structured data (objects, arrays, enums). The specified mime type determines how the data is represented during transfer.
  2. for all other data (internally termed “opaque”), the two headers have no effect. This can be anything from plain Bytes / String / Chunk (with @:header("content-type", "application/pdf") to properly communicate content type) or even a fully formulated OutgoingResponse, in which case you may do what you please.
  3. on opaque data, there’s currently no way to make the client generate particular accept headers.

This would be a solution:

interface RemoteApi {
  @:get('/$id?header-override-content-type=application%2Fpdf')
  function getPdfDocument(id: String):haxe.io.Bytes;
}

To then let the “header override” take effect, you would do this with your client:

client = client.augment({
  before: [req -> new OutgoingRequest(
    {
      var query = Query.build(),
          fields = [for (f in req.header) f],
          url = req.header.url;
      for (param in url.query)
        switch param.name.split('header-override-') {
          case ['', name]:
            fields.push(new HeaderField(name, param.value));
          default:
            query.add(param.name, param.value);
        }

      var url = url.resolve(url.path.toString()) + '?' + query.toString();
      new OutgoingRequestHeader(req.header.method, url, req.header.protocol, fields);
    },
    req.body
  )]
});
2 Likes

I was expecting something a little simpler, but as usual you still provided a solution for the first part (i.e. custom request headers). :hugs:
And for the second part (i.e. having a properly typed response) : it’s really not a big deal to use IncomingResponse.readAll, so I’ll stick to that.
Thank you @back2dos for these clarifications.