Repository: Shopify/hydrogen (TypeScript)

Issue:
Trailing slash redirects are not working as expected according to shopify docs
### What is the location of your example repository?

_No response_

### Which package or tool is having this issue?

Hydrogen

### What version of that package or tool are you using?

2023.10.0

### What version of Remix are you using?

2.1.0

### Steps to Reproduce

1. Create a redirect according to shopify docs with or without trailing slash. Shopify docs say this is handled on their end so the UI will error stating the path already exists if you try adding another one with a slash
2. Try accessing the redirect with and without a trailing slash, e.g. `/about-us` returns redirect but `/about-us/` 404s

### Expected Behavior

The redirect should work with or without the trailing slash.

### Actual Behavior

The redirect only works without the trailing slash.

I traced the problem to the actual query specifically the [`path:` filter](https://github.com/Shopify/hydrogen/blob/ae262b616127a7173d23a1a38a6e658af3105ce8/packages/hydrogen/src/routing/redirect.ts#L62). If you remove that path filter [this query](https://github.com/Shopify/hydrogen/blob/ae262b616127a7173d23a1a38a6e658af3105ce8/packages/hydrogen/src/routing/redirect.ts#L130) returns the correct payload but with that filter it returns this.

```
{
  "data": {
    "urlRedirects": {
      "edges": []
    }
  }
}
```

I'm not sure if this is a bug with the api or hydrogen so I'm filing this here for Shopify folks to chime in.


Discussion:
@johnstew its likely a bit of both. I will report it to the API team and see if there is anything we can do on the hydrogen side for now as well.
Okay, so my workaround was to copy all the code around `storefrontRedirect` and modify it to check if there's a slash at the end and remove it to query for the redirects.

```javascript
const hasTrailingSlash = /\/$/.test(redirectFrom);
const modifiedRedirectFrom = hasTrailingSlash ? redirectFrom.replace(/\/$/, '') : redirectFrom;

const {urlRedirects} = await storefront.query(REDIRECT_QUERY, {
    variables: {
        query: `path:${modifiedRedirectFrom}`,
    },
});
``` 
It looks like Shopify's admin does not allow redirects to be defined with a trailing slash, so it's probably okay for us to automatically strip trailing slashes before we query for redirects. I will make this change.



Patch:
diff --git a/.changeset/large-rivers-fold.md b/.changeset/large-rivers-fold.md
new file mode 100644
index 0000000000..789023601b
--- /dev/null
+++ b/.changeset/large-rivers-fold.md
@@ -0,0 +1,5 @@
+---
+'@shopify/hydrogen': patch
+---
+
+Fix `storefrontRedirect` to strip trailing slashes when querying for redirects. Resolves [#2090](https://github.com/Shopify/hydrogen/issues/2090)
diff --git a/packages/hydrogen/src/routing/redirect.ts b/packages/hydrogen/src/routing/redirect.ts
index 28d171a5b2..b87ec833eb 100644
--- a/packages/hydrogen/src/routing/redirect.ts
+++ b/packages/hydrogen/src/routing/redirect.ts
@@ -59,7 +59,9 @@ export async function storefrontRedirect(
     const {urlRedirects} = await storefront.query<{
       urlRedirects: UrlRedirectConnection;
     }>(REDIRECT_QUERY, {
-      variables: {query: 'path:' + redirectFrom},
+      // The admin doesn't allow redirects to have a
+      // trailing slash, so strip them all off
+      variables: {query: 'path:' + redirectFrom.replace(/\/+$/, '')},
     });
 
     const location = urlRedirects?.edges?.[0]?.node?.target;


Test patch:
diff --git a/packages/hydrogen/src/routing/redirect.test.ts b/packages/hydrogen/src/routing/redirect.test.ts
index 1899c6677e..ed66b23475 100644
--- a/packages/hydrogen/src/routing/redirect.test.ts
+++ b/packages/hydrogen/src/routing/redirect.test.ts
@@ -1,4 +1,4 @@
-import {describe, it, expect, vi} from 'vitest';
+import {describe, it, expect, vi, afterEach} from 'vitest';
 import type {Storefront} from '../storefront';
 import {storefrontRedirect} from './redirect';
 
@@ -10,6 +10,10 @@ describe('storefrontRedirect', () => {
     query: queryMock,
   } as unknown as Storefront;
 
+  afterEach(() => {
+    queryMock.mockReset();
+  });
+
   it('redirects to Shopify admin ', async () => {
     await expect(
       storefrontRedirect({
@@ -46,6 +50,27 @@ describe('storefrontRedirect', () => {
     });
   });
 
+  it('strips trailing slashes on the redirect query', async () => {
+    queryMock.mockResolvedValueOnce({
+      urlRedirects: {edges: [{node: {target: shopifyDomain + '/some-page'}}]},
+    });
+
+    await expect(
+      storefrontRedirect({
+        storefront: storefrontMock,
+        request: new Request('https://domain.com/some-page/'),
+      }),
+    ).resolves.toEqual(
+      new Response(null, {
+        status: 301,
+        headers: {location: shopifyDomain + '/some-page'},
+      }),
+    );
+    expect(queryMock).toHaveBeenCalledWith(expect.anything(), {
+      variables: {query: 'path:/some-page'},
+    });
+  });
+
   it('queries the SFAPI with the url lower cased', async () => {
     queryMock.mockResolvedValueOnce({
       urlRedirects: {edges: [{node: {target: shopifyDomain + '/some-page'}}]},