Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client side JavaScript does not work on app proxy pages #436

Closed
nboliver-ventureweb opened this issue Nov 24, 2023 · 23 comments
Closed

Client side JavaScript does not work on app proxy pages #436

nboliver-ventureweb opened this issue Nov 24, 2023 · 23 comments

Comments

@nboliver-ventureweb
Copy link

nboliver-ventureweb commented Nov 24, 2023

Issue summary

If I create a route in the Remix app with a basic loader and a React component to render as the page content, the page renders properly, but no client side JS works. I tried adding the Scripts component to my route, but no luck.
Perhaps the Remix app doesn't support rendering pages like this?

For example:

import { useLoaderData } from '@remix-run/react';
import { json } from '@remix-run/node';
import { authenticate } from '~/shopify.server';

export async function loader({ request }) {
  console.log(request.headers);
  const { admin, session, liquid } = await authenticate.public.appProxy(
    request,
  );
  
  // Mock data, would actually use queried data here
  return json({
    entries: [
      {
        id: '1',
        name: 'One',
      },
      {
        id: '2',
        name: 'Two',
      },
    ],
  });
}

export default function WishList() {
  const { entries } = useLoaderData();

  // This doesn't do anything
  function handleSubmit(event) {
    alert('test');
    event.preventDefault();
    console.log('Submit', event.currentTarget);
  }

  return (
    <div>
      <h1>Manage Accounts</h1>
      <ul>
        {entries.map(entry => (
          <li key={entry.id}>{entry.name}</li>
        ))}
      </ul>
      <h2>Add Account</h2>
      <form
        onSubmit={handleSubmit}
      >
        <div>
          <label htmlFor="id">Account ID</label>
          <input id="id" name="id" type="text" />
        </div>
        <div>
          <label htmlFor="name">Account Name</label>
          <input id="name" name="name" type="text" />
        </div>
        <button type="submit">
          Submit
        </button>
      </form>
    </div>
  );
}

Expected behavior

Ideally, rendering an app proxy page in the context of the theme by setting Content-Type: application/liquid for the route would work.

Actual behavior

The page is rendered, but client side JS is not included, so it's not really useful.

Steps to reproduce the problem

  1. Set up an app proxy
  2. Navigate to one of your Remix app's routes in front end of the shop (ie. store.myshopify.com/apps/my-app/wishlist)
  3. See the html content of your route, but no JS
@nboliver-ventureweb
Copy link
Author

@paulomarg @lizkenyon Would be awesome if you could confirm whether there's a way to load client side JS on app proxy pages, as it would be quite useful for us 🙏

@nboliver-ventureweb
Copy link
Author

@lizkenyon Any update here?

@paulomarg
Copy link
Contributor

paulomarg commented Feb 13, 2024

Hey, sorry for the delay in responding - currently, JS won't load in proxies because remix will attempt to load the JS assets from /build which won't work going through the proxy.

I'm looking into a way of forcing the URL in those requests to the app's URL, but haven't found one yet. I believe you might be able to load javascript if you use a liquid response (using the liquid helper) to load the JS code you need to run.

I'll circle back if I figure out a different way to get that to work!

@nboliver-ventureweb
Copy link
Author

@paulomarg Thanks for the update. Would be awesome to get this working out of the box with Remix. I'll test out the liquid helper and see how that goes.

@matthaake
Copy link

matthaake commented Feb 16, 2024

I also just ran into this issue with the /build directory not loading in the client with app proxy. I've tried a few things but no luck..

Attempt 1
I had the idea of changing the publicPath in the config to the absolute url of the build directory on my server (ex https://my-remix-server.com/build/) but then I get CORS errors because the requests are coming from the shopify url. I'm stuck there because I can't figure out how to configure CORS on the shopify remix server?

Attempt 2
Same as above but use the absolute path of the proxy url for the build (ex. https://mystore.myshopify.com/apps/proxy-subpath/build/). This loads without cors errors but then I get hydration errors

Initial URL (/myapp) does not match URL at time of hydration (/apps/proxy-subpath/myapp), reloading page...

I'm not sure if this would still be an issue if I got around the CORS errors with attempt 1?

@paulomarg if you have any thoughts on this / if I'm just wasting my time please let me know!!

Update

Attempt 3
I tried uploading the build directory to an s3 bucket to host it separately with an open CORS policy. I received the same hydration error as attempt 2. So I don't believe that fixing the CORS error in attempt 1 would do anything.

This is a pretty disappointing and renders remix relatively useless for building user facing apps through app proxies, aside from authentication.

Update 2

I tested including javascript inside the liquid() helper and that does work. Returning a full webpage of liquid inside a string with javascript is much uglier than working with remix components and jsx though. Would be amazing if there is a workaround to get hydration working with the proxy!

@paulomarg
Copy link
Contributor

paulomarg commented Feb 20, 2024

Hey @matthaake, those are the same things I tried as well, and I think the only way to make that work would be to write a custom Remix server.

I believe we'd be able to set the CORS headers for asset requests to make them work if we set up an express server with express.cors + express.static, but I'm unsure if that isn't too complicated for the template.

That would be useful as documentation in any case, IMO. I'm not sure when I'll have time to prototype that, but if you have a chance to and reach something that works, we could figure out the best way of including it in the template.

The only downside to doing that is that Remix is in the process of moving over to Vite as a server, which means this would need to be adapted to the vite server when it becomes stable and we start using it in the template.

Thank you so much for trying things and for sharing them with us, it's much appreciated!

@paulomarg
Copy link
Contributor

paulomarg commented Feb 21, 2024

Just a quick update: now that the template is running on Vite, I was able to get this to almost work by adding this to my vite.config.ts file:

export default defineConfig({
  base: process.env.SHOPIFY_APP_URL || "/",
  // ...
});

This way, we're forcing the assets to load with the full URL, but there is still one issue with Remix: this raises a hydration error, unless the path in the proxy matches the one in the app exactly. For instance, if you configure your proxy at /apps/proxy, Shopify will send the request to your app using /apps/proxy/.

With that config, if you visit https://<shop>.myshopify.com/apps/proxy/, it will load JS files properly. I'm looking into whether it would be possible to fix this hydration issue to fully unlock proxies, but I believe it should be possible.

@matthaake
Copy link

Awesome, thank you for looking into this!

@mtn-constantinebr
Copy link

mtn-constantinebr commented Feb 28, 2024

Same here, I'm trying out the new Remix template, did the same thing how it written in the article https://shopify.dev/docs/apps/getting-started/build-qr-code-app?framework=remix

But I have 404 error on public pages for JS and CSS files because of relative path is wrong (that works through proxy)

@brophdawg11 This looks urgent here, because it's not working solution right on the documentation. I see your answer remix-run/remix#6785 (comment)

But I didn't understand how to use this.

@sameerabit
Copy link

Any progress with the findings? We have tried all above suggestions but couldn't able to accomplish the needs. I'm following up.

@paulomarg
Copy link
Contributor

Hey, thanks for pinging here. Unfortunately, Remix doesn't support URL rewrites, which makes proxies hard to work with. However, we just introduced some components that should hopefully make it easier to create proxies - more info in this comment: Shopify/shopify-app-js#455 (comment)

Note that this comes with a few caveats, but please try it out after the next release!

@patrick-schneider-latori

Has anyone now a running example on production? This really gives me headaches..

@patrick-schneider-latori

I was able to at least proxy the assets to the correct proxied app url with the help of a express server upfront, but then Remix errors with Initial URL (/hello-world) does not match URL at time of hydration (/a/proxy/hello-world), reloading page... and reloads forever

@paulomarg
Copy link
Contributor

paulomarg commented Apr 8, 2024

Yes @patrick-schneider-latori , unfortunately because of the lack of rewrite support, you'll run into that issue. Right now, you have to do 2 things:

  • Make sure the proxy path you configured (https://<my-store>.myshopify.com/apps/my-proxy) matches your internal path (https://<my-app>/apps/my-proxy) so Remix doesn't consider it a rewrite
  • Make sure you add a trailing slash in your browser when loading the page (https://<my-store>.myshopify.com/apps/my-proxy/). I've forwarded this to the Remix team, hopefully we'll find a fix for this.

Hope this helps!

@patrick-schneider-latori
Copy link

patrick-schneider-latori commented Apr 8, 2024

Thanks for the explanation @paulomarg – let me replay your recommendation, so that all the other people here around understand what you are recommending.

Start fresh install yarn create @shopify/app, then configure shopify.app.toml and add an app_proxy entry with example subpath = "my-proxy" and prefix = "apps".

Then I go into the Remix routes folder and create a new file, e.g. apps.my-proxy.hello.jsx, then insert test content like this:

import { authenticate } from "../shopify.server";
import { useEffect } from "react";

export const loader = async ({ request }) => {
  await authenticate.public.appProxy(request);

  return null;
};

export default function App() {
  useEffect(() => {
    console.log('it works');
  }, []);

  return (
    <div>
      hello my-proxy
    </div>
  );
}

The useEffect in this example is used to see if the hydration works.

When I now head to https://<my-store>.myshopify.com/apps/my-proxy/hello I receive a 404, which is ok, because originally it normally uses the hello.jsx route file of Remix.

So I need now to tell the dev server to take this url and forward it also to the correct Remix file. So I open now vite.config.js and add the following config to my defineConfig function:

base: process.env.SHOPIFY_APP_URL,
server: {
  proxy: {
    '/hello': {
      target: process.env.SHOPIFY_APP_URL,
      changeOrigin: true,
      rewrite: (path) => {
        return path.replace(/^\/hello/, 'apps/my-proxy/hello')
      },
    },
  },
},

When I now head to https://<my-store>.myshopify.com/apps/my-proxy/hello it shows me the following result:

Bildschirmfoto 2024-04-08 um 16 52 24

So I can check this as solved.

Now I try to do the same on production mode. Let's assume the production deployment will resolve on the domain "https://example.ngrok.io".

I update my urls on the shopify.app.toml (application_url, redirect_urls and app_proxy url), deploy the new config to my Shopify App, then I use yarn shopify app env show to get the current credentials of my Shopify app, open the .env file and copy them over, additionally I also enter SHOPIFY_APP_URL="https://example.ngrok.io" – then I trigger the new build with yarn build && yarn start.

I install the Shopify app in my production store, opening the app also in the Shopify dashboard to get a fresh offline token in the database.

Then I reopen the endpoint again on https://<my-production-store-domain>/apps/my-proxy/hello and whops – I got a 404. Of course, because the proxy settings are only valid for the development server.

So I install Express and create a new server.js entry file (using the official example from https://github.com/remix-run/remix/blob/main/templates/express/server.js) and tweaking it a little bit so that I get the following result:

import { createRequestHandler } from "@remix-run/express";
import { installGlobals } from "@remix-run/node";
import express from "express";
import { createProxyMiddleware } from 'http-proxy-middleware';
import cors from "cors";

installGlobals();

const viteDevServer =
  process.env.NODE_ENV === "production"
    ? undefined
    : await import("vite").then((vite) =>
        vite.createServer({
          server: { middlewareMode: true },
        })
      );

const remixHandler = createRequestHandler({
  build: viteDevServer
    ? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
    : await import("./build/server/index.js"),
});

const app = express();

app.disable("x-powered-by");
app.use(cors());

if (viteDevServer) {
  app.use(viteDevServer.middlewares);
} else {
  app.use(
    "/assets",
    express.static(
      "build/client/assets",
      {
        immutable: true,
        maxAge: "1y",
      }
    )
  );
}

app.use(
  express.static(
    "build/client", { 
      maxAge: "1h"
    }
  )
);

app.use(
  '/hello/',
  createProxyMiddleware({
    target: process.env.SHOPIFY_APP_URL + '/apps/my-proxy/hello',
    changeOrigin: true,
  }),
);

app.all("*", remixHandler);

const port = process.env.PORT || 3000;
app.listen(port, () =>
  console.log(`Express server listening at http://localhost:${port}`)
);

I also adjust my start script in package.json to

"start": "npm run setup && cross-env NODE_ENV=production node ./server.js"

And the vite.config.js also needs an update:

base: "https://example.ngrok.io/",
build: {
  assetsDir: "apps/proxy/assets",
}

I found out, that the environment variable process.env.SHOPIFY_APP_URL is set to undefined, although defined in the .env file. The build then reverts back to "/", which makes the proxy useless here.

The direction of the assets also has to be changed, so that the proxy can also take care about them on request.

And here is the result:

Bildschirmfoto 2024-04-08 um 17 45 54

This took my now more than three full days of work to get somehow working.

What a waste of time for something which should normally be handled by Remix/Shopify automatically.

Hopefully this information helps a lot more people around here to get this app proxy working.

@patrick-schneider-latori
Copy link

patrick-schneider-latori commented Apr 8, 2024

I would like to add one thing here. You can adjust the createProxyMiddleware in the Express server to a more generic use of urls, if you are using more than one specific path:

app.use(
  createProxyMiddleware({
    target: process.env.SHOPIFY_APP_URL,
    changeOrigin: true,
    pathFilter: '/hello/',
    pathRewrite: function (path, req) { 
      return path.replace('/hello/', '/apps/my-proxy/hello/')
    }
  }),
);

So any of the following urls are possible:

https://<my-production-store-domain>/apps/my-proxy/hello/
needs Remix route file -> apps.my-proxy.hello.jsx

https://<my-production-store-domain>/apps/my-proxy/hello/world/
needs Remix route file -> apps.my-proxy.hello.world.jsx

https://<my-production-store-domain>/apps/my-proxy/hello/world/123/
needs Remix route file -> apps.my-proxy.hello.world.$id.jsx

@paulomarg
Copy link
Contributor

paulomarg commented Apr 11, 2024

Thanks for expanding on this, I appreciate the extra context for other folks coming in.

I agree that right now there is some awkwardness in setting this up, but unfortunately without URL rewrites there's not a lot we can do (without custom servers), as this very much is a rewrite. Hopefully we'll be able to solve this problem more elegantly in the future.

That being said, we've also added some components specifically for app proxies (see AppProxyProvider) which can be used in the plain app template, as long as you follow the rules I mentioned above (the URL path needs to match and it needs a trailing slash for the first request).

We've also recently put out a wiki page on using a custom server in the template.

Hopefully these will help others get started with proxies quicker. Since you recently went through a similar process, do you see anything missing in these docs that you think we ought to add? Maybe something that goes from the wiki custom server to one that supports a proxy?

@paulomarg
Copy link
Contributor

Since I believe we addressed the original issue here with the new components and fixes, I'm going to close this issue. Please create a new one if you still run into problems.

@nboliver-ventureweb
Copy link
Author

Thanks @paulomarg. Looking forward to testing the new components and fixes out.

@coffeeappsshopify
Copy link

I would like to add one thing here. You can adjust the createProxyMiddleware in the Express server to a more generic use of urls, if you are using more than one specific path:

app.use(
  createProxyMiddleware({
    target: process.env.SHOPIFY_APP_URL,
    changeOrigin: true,
    pathFilter: '/hello/',
    pathRewrite: function (path, req) { 
      return path.replace('/hello/', '/apps/my-proxy/hello/')
    }
  }),
);

So any of the following urls are possible:

https://<my-production-store-domain>/apps/my-proxy/hello/
needs Remix route file -> apps.my-proxy.hello.jsx

https://<my-production-store-domain>/apps/my-proxy/hello/world/
needs Remix route file -> apps.my-proxy.hello.world.jsx

https://<my-production-store-domain>/apps/my-proxy/hello/world/123/
needs Remix route file -> apps.my-proxy.hello.world.$id.jsx

I'm looking for this for a long time, thanks.
But, do you know how to use proxy app with full page render inside template using liquid content-type pr something?

@matthaake
Copy link

That being said, we've also added some components specifically for app proxies (see AppProxyProvider) which can be used in the plain app template, as long as you follow the rules I mentioned above (the URL path needs to match and it needs a trailing slash for the first request).

@paulomarg I tested this with a very simple reactive page, but it does not work because there are still cors errors loading the javascript files. The page renders, but I cannot get any reactivity.

import {authenticate} from '~/shopify.server';
import {AppProxyProvider} from '@shopify/shopify-app-remix/react';
import { LoaderFunctionArgs, json } from '@remix-run/node';
import { Scripts, useLoaderData } from '@remix-run/react';
import { useState } from 'react';

export async function loader({ request }: LoaderFunctionArgs) {
  await authenticate.public.appProxy(request);

  return json({ appUrl: process.env.SHOPIFY_APP_URL! });
}

export default function App() {
  const { appUrl } = useLoaderData<typeof loader>();
  const [count, setCount] = useState(0);

  return (
    <html>
      <body>
        <AppProxyProvider appUrl={appUrl}>
          <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
              Click me
            </button>
          </div>
        </AppProxyProvider>

        <Scripts />
      </body>
    </html>
  );
}

In the console I get
Access to script at 'https://stock-panama-fossil-baths.trycloudflare.com/build/_shared/chunk-BOXFZXVX.js' from origin 'https://<mystore>.myshopify.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Is it possible to add CORS headers from the server that comes out of the box, or do we still need to set up a custom server even with the AppProxyProvider?

@paulomarg
Copy link
Contributor

I added your route to a brand new app and pointed a proxy to it, and I didn't get any such errors 🤔

  • What browser did you use?
  • Did you point your browser to https://<shop>/<path-to-proxy>/?

For me, I could simplify that page to

import { authenticate } from "~/shopify.server";
import { AppProxyProvider } from "@shopify/shopify-app-remix/react";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { useState } from "react";

export async function loader({ request }: LoaderFunctionArgs) {
  await authenticate.public.appProxy(request);

  return json({ appUrl: process.env.SHOPIFY_APP_URL! });
}

export default function App() {
  const { appUrl } = useLoaderData<typeof loader>();
  const [count, setCount] = useState(0);

  return (
    <AppProxyProvider appUrl={appUrl}>
      <div>
        <p>You clicked {count} times</p>
        <button onClick={() => setCount(count + 1)}>Click me</button>
      </div>
    </AppProxyProvider>
  );
}

and it still worked.

@kalenjordan
Copy link

@paulomarg I'm getting it to work when returning content-type html. But running into issues with the liquid response.

If I just return liquid('hello') in the loader then it works. But I can't return jsx components in the loader.

But I need to use jsx components. How can I do that?

I also tried responseHeaders.set("Content-Type", "application/liquid"); in entry.server.jsx. That got the markup to render inside the site template, but again hitting the asset 404 loading issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants