We recently enabled expiring offline access tokens (future: { expiringOfflineAccessTokens: true }) in our React Router app, as required for public apps.
Since then, our app/uninstalled and mandatory compliance webhooks (shop/redact etc.) fail with a 500 on every delivery and retry. All other webhooks work fine, and bad-HMAC requests correctly get a 401. Our handlers follow the template docs exactly, including the if (!session) check.
What we found: authenticate.webhook() tries to refresh the shop’s access token before our handler code runs. But when a shop uninstalls, Shopify revokes their tokens - so the refresh can never succeed, and the library responds with a 500 instead of continuing with session: undefined like the docs describe. Our handler never runs, so we never get to return the 200.
It only happens when the stored token is already stale (shop idle for an hour or more before uninstalling). Uninstalling right after using the app works fine - which is why our own testing never caught it, but the app review store hit it.
Reproduce:
- Enable
expiringOfflineAccessTokens - Install the app, don’t touch it for an hour
- Uninstall without opening the app
app/uninstalled→ 500, andshop/redactlater fails the same way
This is a problem for app review, since compliance webhooks must return a 200.
Has anyone else run into this since switching to expiring tokens? Is there a recommended way to handle webhooks for shops whose tokens are already revoked?
Or are we doing something completely wrong here?
export const action = async ({ request }: ActionFunctionArgs) => {
const { shop, session, topic } = await authenticate.webhook(request);
console.log(`Received ${topic} webhook for ${shop}`);
if (!session) {
return new Response();
}
await db.session.deleteMany({ where: { shop } });
return new Response();
};