Skip to content
duarteclemente
Go back

Graph's Convert-to-PDF Doesn't Return a PDF

I needed to turn a Word document sitting in SharePoint into a PDF from inside an Azure Function. No user in the loop, no interactive session — just a bit of server-side code that grabs a .docx and hands back a .pdf. Microsoft Graph advertises exactly this: request the drive item’s content with ?format=pdf and Graph does the conversion for you. Sounds like a one-liner. It is not a one-liner, and the naive version quietly leaks your access token.

Table of contents

Open Table of contents

The API that doesn’t do what it says

Here’s the call Graph tells you to make:

GET /drives/{drive-id}/items/{item-id}/content?format=pdf

You’d expect PDF bytes in the response body. You don’t get them. What comes back is a 302 Found with a Location header pointing at a pre-authenticated Azure storage URL. That’s where the actual PDF lives. Graph does the conversion, stashes the result somewhere, and redirects you to it.

Fine — a redirect. HttpClients follow redirects. So the “obvious” code is: call Graph with your bearer token, let the client follow the 302, read the body. Done.

That’s the trap.

Where the token leaks

By default, HttpClient follows redirects automatically. When it follows that 302, it takes the Authorization header you set for the Graph request and forwards it to the redirect target — the storage endpoint. So your Graph bearer token, minted for graph.microsoft.com, gets sent to a completely different host that never asked for it and doesn’t need it. The pre-authenticated URL already carries its own short-lived credential in the query string; your token is pure surplus, handed to an endpoint outside your trust boundary.

Nothing errors. You get your PDF. The code looks like it works, and it does work — while also spraying a Graph access token at a storage service on every single call. It’s the kind of thing that never shows up in a smoke test and lights up in a security review.

The lesson underneath it: “follows redirects” is a security decision, not just a convenience setting. The moment a redirect can cross hosts, auto-following means auto-forwarding your credentials to wherever the server points you.

The fix: two HttpClients

The clean fix is to stop treating this as one request. It’s two, and they talk to two different worlds, so they get two different clients.

Client one is a named client with auto-redirect turned off. It carries the Authorization header and talks to Graph. When Graph answers with the 302, the client hands you the response instead of chasing it. You read the Location header yourself.

new HttpClientHandler { AllowAutoRedirect = false }

Client two is unauthenticated. No bearer token, nothing. It takes that Location URL and downloads the PDF bytes. The storage endpoint is already pre-authenticated by the URL itself, so a plain client is all it needs — and your Graph token never leaves the request it was minted for.

Two clients, one boundary each. The token stays where it belongs. You still get your PDF. The only thing you lose is the credential leak.

The bonus gotcha: you can’t chain the addressing

While I was in there, Graph handed me a second one. To get to the drive item I first had to point at the SharePoint site, and Graph lets you address a site by hostname and path with colon-syntax:

GET /sites/{hostname}:/sites/<site>:

It also lets you address a drive item by path with the root:/…:/content colon-syntax. So the tidy instinct is to glue them into a single call — resolve the site and reach the file in one round trip. Graph rejects it:

Resource not found for the segment 'root:'

You can’t combine the sites/{host}:{path}: colon-addressing with root:/…:/content in the same request. The two colon-scoped segments don’t compose. So it’s two calls, not one: resolve the site to its id first, then use that id to address the drive and the item. Slightly annoying, entirely consistent once you accept that colon-addressing is a top-level convenience, not something you can nest.

The takeaway

Both of these are the same shape of bug: an API that looks like it does one clean thing, and a default that quietly does something you didn’t ask for. The convert-to-PDF endpoint doesn’t return a PDF, it returns a pointer — and the pointer is where your token goes to leak if you let your client follow it blindly.

So when I wire up any Graph call that can redirect now, the first thing I check isn’t the happy path. It’s where the Authorization header ends up.


Slides: the condensed version is in a short deck — download the PDF.

When did you last check where your HttpClient forwards your token after a redirect?


Share this post:

Previous Post
Five Gotchas That Make a Power Apps Code App Actually Run
Next Post
A Time Clock That Catches Labor Issues Before Payroll