Skip to content

Commit

Permalink
Fix hmr assetPrefix escaping and reuse logic from other files (#67983)
Browse files Browse the repository at this point in the history
<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->

### Fixing a bug

fixes #63820
fixes #55320
fixes #52931

In one of the issues, there was a suggestion to delete `assetPrefix`
from `next.config`. It helps if you have `assetPrefix: '/'`. Otherwise,
you probably won't face this problem.

**Explanation:**

The problem lies where the browser tries to connect and where websocket
is available on the server.

Adjust the condition for `prefix` in `get-socket-url` as it can return
`''` which will lead to `//` in the url.

The browser wants to connect to `ws://localhost:3000/_next/webpack-hmr`
While internally next exposes the web socket under
`ws://localhost:3000//_next/webpack-hmr` - you can connect to it via
wscat, postman or whatever.
As the path is different it does not handle HMR requests in the browser.

In addition to that - Reuse logic and extract separate files as a helper
in shared/lib.

| Before | After |
| ------ | ------- |
| ![before - latest canary
branch](https://github.com/user-attachments/assets/c26c8b20-1352-49c6-a099-101394438ba0)
before - latest canary branch - we can't establish a connection to HMR
ws |
![after](https://github.com/user-attachments/assets/26500e68-bc4d-44ca-b418-f9bda6bc9aa6)
same example with local changes - connected to ws |

---------

Co-authored-by: Jiwon Choi <devjiwonchoi@gmail.com>
Co-authored-by: JJ Kasper <jj@jjsweb.site>
  • Loading branch information
3 people committed Jul 30, 2024
1 parent 87464d0 commit c18fcf2
Show file tree
Hide file tree
Showing 5 changed files with 866 additions and 925 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { normalizedAssetPrefix } from '../../../../../shared/lib/normalized-asset-prefix'

function getSocketProtocol(assetPrefix: string): string {
let protocol = window.location.protocol

Expand All @@ -9,18 +11,16 @@ function getSocketProtocol(assetPrefix: string): string {
return protocol === 'http:' ? 'ws' : 'wss'
}

export function getSocketUrl(assetPrefix: string): string {
export function getSocketUrl(assetPrefix: string | undefined): string {
const { hostname, port } = window.location
const protocol = getSocketProtocol(assetPrefix)
const normalizedAssetPrefix = assetPrefix.replace(/^\/+/, '')

let url = `${protocol}://${hostname}:${port}${
normalizedAssetPrefix ? `/${normalizedAssetPrefix}` : ''
}`
const protocol = getSocketProtocol(assetPrefix || '')
const prefix = normalizedAssetPrefix(assetPrefix)

if (normalizedAssetPrefix.startsWith('http')) {
url = `${protocol}://${normalizedAssetPrefix.split('://', 2)[1]}`
// if original assetPrefix is a full URL with protocol
// we just update to use the correct `ws` protocol
if (assetPrefix?.replace(/^\/+/, '').includes('://')) {
return `${protocol}://${prefix}`
}

return url
return `${protocol}://${hostname}:${port}${prefix}`
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
import type { HMR_ACTION_TYPES } from '../../../../server/dev/hot-reloader-types'
import { getSocketUrl } from '../internal/helpers/get-socket-url'

let source: WebSocket

type ActionCallback = (action: HMR_ACTION_TYPES) => void

const eventCallbacks: Array<ActionCallback> = []

function getSocketProtocol(assetPrefix: string): string {
let protocol = location.protocol

try {
// assetPrefix is a url
protocol = new URL(assetPrefix).protocol
} catch {}

return protocol === 'http:' ? 'ws' : 'wss'
}

export function addMessageListener(callback: ActionCallback) {
eventCallbacks.push(callback)
}
Expand Down Expand Up @@ -62,17 +52,7 @@ export function connectHMR(options: { path: string; assetPrefix: string }) {
timer = setTimeout(init, reconnections > 5 ? 5000 : 1000)
}

const { hostname, port } = location
const protocol = getSocketProtocol(options.assetPrefix || '')
const assetPrefix = options.assetPrefix.replace(/^\/+/, '')

let url = `${protocol}://${hostname}:${port}${
assetPrefix ? `/${assetPrefix}` : ''
}`

if (assetPrefix.startsWith('http')) {
url = `${protocol}://${assetPrefix.split('://', 2)[1]}`
}
const url = getSocketUrl(options.assetPrefix)

source = new window.WebSocket(`${url}${options.path}`)
source.onopen = handleOnline
Expand Down
9 changes: 8 additions & 1 deletion packages/next/src/server/lib/router-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
HMR_ACTIONS_SENT_TO_BROWSER,
type AppIsrManifestAction,
} from '../dev/hot-reloader-types'
import { normalizedAssetPrefix } from '../../shared/lib/normalized-asset-prefix'

const debug = setupDebug('next:router-server:main')
const isNextFont = (pathname: string | null) =>
Expand Down Expand Up @@ -662,8 +663,14 @@ export async function initialize(opts: {
if (opts.dev && developmentBundler && req.url) {
const { basePath, assetPrefix } = config

let hmrPrefix = basePath

// assetPrefix overrides basePath for HMR path
if (assetPrefix) {
hmrPrefix = normalizedAssetPrefix(assetPrefix)
}
const isHMRRequest = req.url.startsWith(
ensureLeadingSlash(`${assetPrefix || basePath}/_next/webpack-hmr`)
ensureLeadingSlash(`${hmrPrefix}/_next/webpack-hmr`)
)

// only handle HMR requests if the basePath in the request
Expand Down
16 changes: 16 additions & 0 deletions packages/next/src/shared/lib/normalized-asset-prefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function normalizedAssetPrefix(assetPrefix: string | undefined): string {
const escapedAssetPrefix = assetPrefix?.replace(/^\/+/, '') || false

// assetPrefix as a url
if (escapedAssetPrefix && escapedAssetPrefix.startsWith('://')) {
return escapedAssetPrefix.split('://', 2)[1]
}

// assetPrefix is set to `undefined` or '/'
if (!escapedAssetPrefix) {
return ''
}

// assetPrefix is a common path but escaped so let's add one leading slash
return `/${escapedAssetPrefix}`
}
Loading

0 comments on commit c18fcf2

Please sign in to comment.