Skip to content

fetch() fails or returns an opaque response for a redirected cross-origin URL #4740

@cdoublev

Description

@cdoublev

Bug Description

fetch() fails or returns an opaque response when it takes a redirected cross-origin URL, while it always returns a basic (transparent) response when the cross-origin URL is not redirected.

Reproducible By

Test results 1-3 are expected. Test results 4-9 are not?

import http from 'http'
import { fetch, setGlobalOrigin } from 'undici'

function createServer(same, cross) {
  http.createServer((req, res) => {
    if (req.url === '/redirected') {
        return res.writeHead(301, { Location: `http://localhost:${cross}` }).end()
    }
    return res.writeHead(200).end()
  })
  .listen(same)
}

createServer(8080, 8081)
createServer(8081, 8080)

const origin = 'http://localhost:8080'
const crossOrigin = 'http://localhost:8081'

setGlobalOrigin(origin)

const tests = [
    // Not redirected
    [`${crossOrigin}/foo`],                                 // 1. basic
    [`${crossOrigin}/foo`, { mode: 'same-origin' }],        // 2. basic
    [`${crossOrigin}/foo`, { mode: 'no-cors' }],            // 3. basic
    // Redirected from cross-origin to same-origin
    [`${crossOrigin}/redirected`],                          // 4. cors
    [`${crossOrigin}/redirected`, { mode: 'same-origin' }], // 5. fetch failed
    [`${crossOrigin}/redirected`, { mode: 'no-cors' }],     // 6. opaque
    // Redirected from same-origin to cross-origin
    [`${origin}/redirected`],                               // 7. cors
    [`${origin}/redirected`, { mode: 'same-origin' }],      // 8. fetch failed
    [`${origin}/redirected`, { mode: 'no-cors' }],          // 9. opaque
]
.map(async ([url, init]) => {
    try {
        const response = await fetch(url, init)
        return response.type
    } catch (e) {
        return e.message
    }
})
const results = await Promise.all(tests)
console.log(results)

Note that for browsers, returning an opaque response to a request of a cross-origin URL redirected to a same-origin URL is expected (whatwg/fetch#737).

Expected Behavior

I would expect fetch() to allow requests to any origin regardless of any redirection:

Unlike browsers, Undici does not implement CORS (Cross-Origin Resource Sharing) checks by default. This means:

  • [...]
  • Requests to any origin are allowed regardless of the source

https://undici.nodejs.org/#/?id=specification-compliance

Logs & Screenshots

Environment

Node v24.12.0, Undici v7.18.2

Additional context

While debugging undici.fetch() taking a redirected URL, this line in mainFetch() intrigued me, because it does not match the origin of the current URL with request.origin, as required in the spec, but against the origin of request.url:

request’s current URL’s origin is same origin with request’s origin, and request’s response tainting is "basic"

I am not very familiar with the Fetch specification but I guess this difference is intentional.

Now assuming consistency between non-redirected and redirected cross-origin request is a valid expectation, I do not know how much undici could be allowed to deviate from the specification.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions