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

feat(client): improved basepath detection #2320

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/client/__tests__/parse-url.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import parseUrl from "../../lib/parse-url"

// https://stackoverflow.com/questions/48033841/test-process-env-with-jest
describe("parseUrl() tests", () => {
const OLD_ENV = process.env

beforeEach(() => {
jest.resetModules() // Most important - it clears the cache
process.env = { ...OLD_ENV } // Make a copy
})

afterAll(() => {
process.env = OLD_ENV // Restore old environment
Comment on lines +8 to +13
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's no need for comments here, the code is pretty self-descriptive 👍🏽

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

})

test("when on client side and NEXT_PUBLIC_NEXTAUTH_URL is defined, correctly returns baseUrl and basePath", () => {
process.env.NEXT_PUBLIC_NEXTAUTH_URL = "http://localhost:3000/api/v1/auth"

const { baseUrl, basePath } = parseUrl()

expect(baseUrl).toBe("http://localhost:3000")
expect(basePath).toBe("/api/v1/auth")
})

test("when on client side and NEXT_PUBLIC_NEXTAUTH_URL is not defined, falls back to default values", () => {
const { baseUrl, basePath } = parseUrl()

expect(baseUrl).toBe("http://localhost:3000")
expect(basePath).toBe("/api/auth")
})

test("when on server side, correctly parses any given URL", () => {
let url = "http://localhost:3000"
let { baseUrl, basePath } = parseUrl(url)

expect(baseUrl).toBe("http://localhost:3000")
expect(basePath).toBe("/api/auth")

url = "http://localhost:3000/api/v1/authentication/"
// this semi-colon is needed, otherwise js thinks the string
// above is a function
;({ baseUrl, basePath } = parseUrl(url))
Comment on lines +39 to +42
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand we don't need to re-assign url here, why not just do:

const url1 = "http://localhost:3000"
const { baseUrl: baseUrl1, basePath: basePath1 } = parseUrl(url1)
// ...
const url2 = "http://localhost:3000"
const { baseUrl: baseUrl2, basePath: basePath2 } = parseUrl(url2)

I think will make this case a bit nicer to read 👍🏽

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to do this but decided against it. But it seems like it would actually be better this way.


expect(baseUrl).toBe("http://localhost:3000")
expect(basePath).toBe("/api/v1/authentication")

url = "https://www.mydomain.com"
;({ baseUrl, basePath } = parseUrl(url))

expect(baseUrl).toBe("https://www.mydomain.com")
expect(basePath).toBe("/api/auth")

url = "https://www.mydomain.com/api/v3/auth"
;({ baseUrl, basePath } = parseUrl(url))

expect(baseUrl).toBe("https://www.mydomain.com")
expect(basePath).toBe("/api/v3/auth")
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to add test cases for the unhappy paths as well 🙏🏽

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. That would probably make more sense once we have refactored parseUrl() to throw errors.

})
13 changes: 12 additions & 1 deletion src/client/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function useSession(options = {}) {

React.useEffect(() => {
if (requiredAndNotLoading) {
const url = `/api/auth/signin?${new URLSearchParams({
const url = `${_apiBaseUrl()}/signin?${new URLSearchParams({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could use parseUrl() rather than having duplicate utils?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure on this one.

As far as I understand, _apiBaseUrl() checks the __NEXTAUTH object, which in turn makes use of the parseUrl() function. So technically it could be refactored.

I would leave it on the core team to make decisions on this and refactor accordingly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I didn't realise _apiBaseUrl() calls parseUrl() internally, as you mention worth of a refactor in a different PR 👍🏽

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mahieyin-rahmun could you move this change to a different PR please? 🙏🏽

error: "SessionRequired",
callbackUrl: window.location.href,
})}`
Expand Down Expand Up @@ -339,6 +339,17 @@ function _apiBaseUrl() {
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}

if (
process.env.NODE_ENV === "development" &&
!/^http:\/\/localhost:\d+$/.test(process.env.NEXTAUTH_URL) &&
!process.env.NEXT_PUBLIC_NEXTAUTH_URL
) {
logger.warn(
"NEXT_PUBLIC_NEXTAUTH_URL",
`NEXTAUTH_URL is set to "${process.env.NEXTAUTH_URL}" instead of the default "http://localhost:3000", and NEXT_PUBLIC_NEXTAUTH_URL is not set. Client side path detections will fail.`
)
}

// Return absolute path when called server side
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
}
Expand Down
31 changes: 22 additions & 9 deletions src/lib/parse-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,36 @@
* supporting a default value, so a simple split is sufficent.
* @param {string} url
*/
export default function parseUrl (url) {
export default function parseUrl(url) {
Copy link
Collaborator

@0ubbe 0ubbe Jul 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is also called server-side, and the logic currently works because there we pass it NEXTAUTH_URL.

I think we should move that logic inside, hence whenever calling parseUrl it can automatically compute what the right base URL and base path are:

export default function parseBaseUrl(url) {
// ..
  if (process.env.NEXTAUTH_URL) {
    if (!process.env.NEXT_PUBLIC_NEXTAUTH_URL) {
      throw new Error("[next-auth] Trying to use a custom `NEXTAUTH_URL` without setting `NEXT_PUBLIC_NEXTAUTH_URL`, client-side redirects won't work")

Now that we're modifying this function, I think we should:

  • rename it as it's not generic utility about URLs but is intended to compute the base URL
  • throw in case expectations don't match so we're explicit about mis-configurations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move that logic inside, hence whenever calling parseUrl it can automatically compute what the right base URL and base path are

Yes, currently, it feels like it's dangling between both client and server.

throw in case expectations don't match so we're explicit about mis-configurations

Agreed.

// Default values
const defaultHost = 'http://localhost:3000'
const defaultPath = '/api/auth'

if (!url) { url = `${defaultHost}${defaultPath}` }
const defaultHost = "http://localhost:3000"
const defaultPath = "/api/auth"

if (!url) {
if (process.env.NEXT_PUBLIC_NEXTAUTH_URL) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the impression we should move this to v4 and change NEXTAUTH_URL to be NEXT_PUBLIC_NEXTAUTH_URL. In this way, we can keep a single environment variable and prevent code complexity by having to manage two of them. What do you think @balazsorban44 ? 🧶

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is intended to be part of v4, otherwise it would be a big breaking change, IMO. I made the pull request against the next branch.

I like the idea of having a single variable.

/*
if the user has defined NEXT_PUBLIC_NEXTAUTH_URL, which should be mandatory
if they are using a different path other than 'http://localhost:3000'
in NEXTAUTH_URL variable in their .env file, use that variable instead,
otherwise, the clientside path detection will fail.
*/
url = process.env.NEXT_PUBLIC_NEXTAUTH_URL
} else {
// fallback to default values, user should see a warning if NEXTAUTH_URL is different
// and NEXT_PUBLIC_NEXTAUTH_URL is not set
url = `${defaultHost}${defaultPath}`
}
}
// Default to HTTPS if no protocol explictly specified
const protocol = url.startsWith('http:') ? 'http' : 'https'
const protocol = url.startsWith("http:") ? "http" : "https"

// Normalize URLs by stripping protocol and no trailing slash
url = url.replace(/^https?:\/\//, '').replace(/\/$/, '')
url = url.replace(/^https?:\/\//, "").replace(/\/$/, "")

// Simple split based on first /
const [_host, ..._path] = url.split('/')
const [_host, ..._path] = url.split("/")
const baseUrl = _host ? `${protocol}://${_host}` : defaultHost
const basePath = _path.length > 0 ? `/${_path.join('/')}` : defaultPath
const basePath = _path.length > 0 ? `/${_path.join("/")}` : defaultPath
Comment on lines +29 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, is this something that could be accomplished using URL ?

Something like:

let baseUrl = defaultHost;
let basePath = defaultPath;
try {
  url = new URL(url, url.startsWith('http') ? undefined : defaultHost);
  baseUrl = url.origin;
  basePath = url.pathname.length ? url.pathname : defaultPath;
} catch (e) {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting PoV. I will try and test this out. Looks like this could work, with some modifications.


return { baseUrl, basePath }
}
11 changes: 11 additions & 0 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ if (!process.env.NEXTAUTH_URL) {
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}

if (
process.env.NODE_ENV === "development" &&
!/^http:\/\/localhost:\d+$/.test(process.env.NEXTAUTH_URL) &&
!process.env.NEXT_PUBLIC_NEXTAUTH_URL
) {
logger.warn(
"NEXT_PUBLIC_NEXTAUTH_URL",
`NEXTAUTH_URL is set to "${process.env.NEXTAUTH_URL}" instead of the default "http://localhost:\${PORT}", and NEXT_PUBLIC_NEXTAUTH_URL is not set. Client side path detections will fail.`
)
}
Comment on lines +23 to +32
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, we should move this logic to parseUrl and the check above too to start sliming down this monolith file.

On the other hand, we should be throwing errors in both cases rather than warnings as this misconfiguration, I understand, is critical? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, we should be throwing errors in both cases rather than warnings as this misconfiguration, I understand, is critical?

I agree on this. I could not think of a better message, so I left it as warning, but it should definitely be treated as error.


/**
* @param {import("next").NextApiRequest} req
* @param {import("next").NextApiResponse} res
Expand Down
15 changes: 15 additions & 0 deletions www/docs/getting-started/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ When deploying your site set the `NEXTAUTH_URL` environment variable to the cano
NEXTAUTH_URL=https://example.com
```

If you are using a URL for the API endpoints that is different than this (hence, a different folder structure as well), for example `NEXTAUTH_URL=https://example.com/api/v1/auth`, you also need to set a second environment variable with the key `NEXT_PUBLIC_NEXTAUTH_URL` and point it to the same URL. Otherwise, client side path detections will fail.

```
NEXTAUTH_URL=https://example.com/api/v1/auth
NEXT_PUBLIC_NEXTAUTH_URL=https://example.com/api/v1/auth
```

This assumes that `[...nextauth].js` is inside `/pages/api/v1/auth/`.

You can use a [Next.js feature](https://nextjs.org/docs/basic-features/environment-variables) to refer to environment variables by their names within the file.
```
NEXTAUTH_URL=https://example.com/api/v1/auth
NEXT_PUBLIC_NEXTAUTH_URL=$NEXTAUTH_URL
```

:::tip
In production, this needs to be set as an environment variable on the service you use to deploy your app.

Expand Down
13 changes: 13 additions & 0 deletions www/docs/getting-started/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,17 @@ e.g.
`NEXTAUTH_URL=https://example.com/myapp/api/authentication`

`/api/auth/signin` -> `/myapp/api/authentication/signin`

However, you need to do two things:
1. You must declare a `NEXT_PUBLIC_NEXTAUTH_URL` environment variable that points to the same URL as above.
2. Your folder structure must match what's set on `NEXT_PUBLIC_NEXTAUTH_URL`, for instance, if it's set to `https://mywebsite.com/api/authentication`, your folder structure should be:

```
.
└── pages
└── api
└── authentication
└── [...nextauth].js

```
:::
6 changes: 5 additions & 1 deletion www/docs/warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Warnings

This is a list of warning output from NextAuth.js.

All warnings indicate things which you should take a look at, but do not inhibit normal operation.
All warnings indicate things which you should take a look at, but in most cases do not inhibit normal operation.

---

Expand All @@ -15,6 +15,10 @@ All warnings indicate things which you should take a look at, but do not inhibit

Environment variable `NEXTAUTH_URL` missing. Please set it in your `.env` file.

#### NEXT_PUBLIC_NEXTAUTH_URL

You are using a folder structure and `NEXTAUTH_URL` other than the default one, but you haven't set `NEXT_PUBLIC_NEXTAUTH_URL` or `NEXT_PUBLIC_NEXTAUTH_URL` points to a different endpoint. This will make client side path detections fail if not addressed.

---

## Server
Expand Down