unjs / nitro

Next Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer.
https://nitro.unjs.io
MIT License
5.98k stars 496 forks source link

Support canonical URL #2052

Open pi0 opened 9 months ago

pi0 commented 9 months ago

ref https://github.com/nuxt/nuxt/issues/24813

Today, we expose useRequestURL() utility to access the current request URL however it can be tricky in two situations:

Proposal:

adamdehaven commented 5 months ago

How would you utilize this if the canonical URL is determined at runtime, such as in a multi-tenant environment with dynamic subdomains?

pi0 commented 5 months ago

How would you utilize this if the canonical URL is determined at runtime, such as in a multi-tenant environment with dynamic subdomains?

adamdehaven commented 5 months ago
  • If there is one dynamic canonical per instance, via runtime config

Since we only build once, setting an env variable isn't an option

  • If it is variable across all requests, i think you should rely on the current useRequestURL utility or have your own composable with multi-tanent specific logic handling

I've tried this, but even when setting the varies option, the Nitro server does not always receive the correct URL from useRequestURL:

There doesn't seem to be a consistent way

pi0 commented 5 months ago

setting an env variable isn't an option

You set environment variables when running instance. It is possible to set them after-build actually, hence the benefit of using of runtimeconfig.

I've tried this, but even when setting the varies option, the Nitro server does not always receive the correct URL from useRequestURL:

would be worth to track with another issue if you don't mind to make a minimal reproduction of your setup so i can investigate 🙏🏼

adamdehaven commented 5 months ago

It is possible to set them after-build actually, hence the benefit of using of runtimeconfig.

Have a simple of example of how you would set the runtimeConfig value after build, e.g. when the app first initializes and grab the browser's hostname, etc.?

would be worth to track with another issue if you don't mind to make a minimal reproduction of your setup so i can investigate

Here's a new issue with reproduction and a deployed preview: https://github.com/unjs/nitro/issues/2388

nschipperbrainsmith commented 1 month ago

I would also like to chime in here, I've been spending the better half of 2 days now trying to work around this problem.

What we do: We are using Nuxt to serve two distinct projects that overlap quite a lot and as such they share a lot of components. As it seemed much simpler we decided to let it all be handled by one Nuxt instance with some trickery on the rewrite level of the HTTP server (in our case Caddy). How we achieve this is by having 2 distinct folders in the pages directroy of Nuxt one (for this example) called foldera another called folderb. We achieve this by having a router.options.ts file that rewrites the path based on the fact if it is a subdomain or not.

app/router.options.ts

import type {RouterOptions} from "@nuxt/schema";
import {useRequestHeader, useRequestURL} from '#imports';
import type {RouteRecordRaw} from "vue-router";

const rewritePrefixRoute = (route: RouteRecordRaw, prefix: string) => {
    if (route.path.startsWith(prefix)) {
        return {
            ...route,
            path: route.path.replace(prefix, ""),
        };
    }
    return route;
}

export default <RouterOptions>{
    routes: (routes) => {
        let hostname = useRequestHeader('X-Requested-Host') ?? '';
        console.info('X-Requested-Host:', hostname);
        if (hostname == null || hostname == '') {
            hostname = useRequestURL()['hostname'];
        }
        console.info('Rewriting routes for hostname:', hostname);
        const domainArray = ['domain.local'];
        const rootDomain = domainArray.find(domain => hostname.endsWith(domain));
        if (!rootDomain) {
            return routes;
        }

        const subdomain = hostname.substring(0, hostname.indexOf(rootDomain) - 1);
        if (hostname === rootDomain && subdomain === '') {
            console.info('Rewriting /foldera folder to root');
            return routes.map((route) => rewritePrefixRoute(route, '/foldera'))
        }

        console.info('Rewriting /uvw folder to root');
        return routes
            .map((route) => rewritePrefixRoute(route, '/folderb'))
    },
};

What works: All requests on the main domain work without a problem (foldera)

What doesn't work: All request on the subdomains fail as they seem to prefix the entire URL in front of the resolving route.

So far, I have been able to get caddy to forward the correct headers and have added the described nuxt config:

  routeRules: {
    '/**': {
      cache: { swr: true, varies: ['host', 'x-forwarded-host'] }
    }
  }

Caddyfile

*.{$WWW_SERVER_NAME} {
    @isFile {
        path *.css *.js *.Vue *.html
        path /*
    }

    @isNotFile {
        not path *.css *.js *.Vue *.html
        path /*
    }

    log
    redir /foldera* {scheme}://{labels.2}.{labels.1}.{labels.0}/404

    rewrite @isFile {scheme}://{labels.2}.{labels.1}.{labels.0}{uri}
    rewrite @isNotFile {scheme}://{labels.2}.{labels.1}.{labels.0}{path}

    reverse_proxy {
        to {$WWW_SERVICE_NAME:nuxtjs}:{$WWW_SERVICE_PORT:80}
        # Note that I tried this with or without subdomains
        header_up Host {labels.1}.{labels.0}
        header_up X-Forwarded-Host {labels.1}.{labels.0}
        header_up X-Requested-Host {labels.2}.{labels.1}.{labels.0}
    }
}

{$WWW_SERVER_NAME} {
    log
    redir /folderb* {scheme}://{labels.1}.{labels.0}/404

    reverse_proxy {
        to {$WWW_SERVICE_NAME:nuxtjs}:{$WWW_SERVICE_PORT:80}
        header_up Host {labels.1}.{labels.0}
        header_up X-Forwarded-Host {labels.1}.{labels.0}
        header_up X-Requested-Host {labels.1}.{labels.0}
    }
}

I have also added logging to trace what is actually happening using:

/server/plugins/logging.ts

 export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', (event) => {
    console.info(
      'Incoming request for URL',
      event.node.req.url,
      event.node.req.originalUrl,
      JSON.stringify(event.node.req.rawHeaders)
    )
  })
  nitroApp.hooks.hook('beforeResponse', (event) => {
    console.info(
      'Sending response with status',
      event.node.res.statusCode,
      event.node.req.url,
      event.node.req.originalUrl,
      JSON.stringify(event.node.req.rawHeaders),
      JSON.stringify(event.node.res.getHeaders())
    )
  })
})

This is what the logs show:

main domain (foldera):

Incoming request for URL / / ["Host","domain.local","User-Agent","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36","Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding","gzip, deflate, br, zstd","Accept-Language","en-US,en;q=0.9,nl;q=0.8","Cache-Control","no-cache","Cookie","i18n_redirected=nl","Pragma","no-cache","Priority","u=0, i","Sec-Ch-Ua","\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"","Sec-Ch-Ua-Mobile","?0","Sec-Ch-Ua-Platform","\"Linux\"","Sec-Fetch-Dest","document","Sec-Fetch-Mode","navigate","Sec-Fetch-Site","none","Sec-Fetch-User","?1","Upgrade-Insecure-Requests","1","X-Forwarded-For","172.19.0.1","X-Forwarded-Host","domain.local","X-Forwarded-Proto","https","X-Requested-Host","domain.local"]
2024-09-06T10:33:20.362413727Z X-Requested-Host: 
2024-09-06T10:33:20.362576853Z Rewriting routes for hostname: domain.local
2024-09-06T10:33:20.362601149Z Rewriting /foldera folder to root
2024-09-06T10:33:20.378597587Z App: Current locale: nl

sub domain(folderb):

Incoming request for URL https://subdomain.example.local/ https://subdomain.example.local/ ["Host","example.local","User-Agent","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36","Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding","gzip, deflate, br, zstd","Accept-Language","en-US,en;q=0.9,nl;q=0.8","Cache-Control","no-cache","Cookie","i18n_redirected=nl","Pragma","no-cache","Priority","u=0, i","Sec-Ch-Ua","\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"","Sec-Ch-Ua-Mobile","?0","Sec-Ch-Ua-Platform","\"Linux\"","Sec-Fetch-Dest","document","Sec-Fetch-Mode","navigate","Sec-Fetch-Site","none","Sec-Fetch-User","?1","Upgrade-Insecure-Requests","1","X-Forwarded-For","172.19.0.1","X-Forwarded-Host","example.local","X-Forwarded-Proto","https","X-Requested-Host","subdomain.example.local"]
2024-09-06T10:30:33.575387614Z Incoming request for URL /__nuxt_error?url=https:%2F%2Fsubdomain.example.local%2F&statusCode=404&statusMessage=Cannot+find+any+route+matching+https:%2F%2Fsubdomain.example.local%2F.&message=Cannot+find+any+route+matching+https:%2F%2Fsubdomain.example.local%2F.&stack /__nuxt_error?url=https:%2F%2Fsubdomain.example.local%2F&statusCode=404&statusMessage=Cannot+find+any+route+matching+https:%2F%2Fsubdomain.example.local%2F.&message=Cannot+find+any+route+matching+https:%2F%2Fsubdomain.example.local%2F.&stack ["host","example.local","user-agent","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36","accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","accept-encoding","gzip, deflate, br, zstd","accept-language","en-US,en;q=0.9,nl;q=0.8","cache-control","no-cache","cookie","i18n_redirected=nl","pragma","no-cache","priority","u=0, i","sec-ch-ua","\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"","sec-ch-ua-mobile","?0","sec-ch-ua-platform","\"Linux\"","sec-fetch-dest","document","sec-fetch-mode","navigate","sec-fetch-site","none","sec-fetch-user","?1","upgrade-insecure-requests","1","x-forwarded-for","172.19.0.1","x-forwarded-host","example.local","x-forwarded-proto","https","x-requested-host","subdomain.example.local","x-nuxt-error","true"]
2024-09-06T10:30:33.576433488Z X-Requested-Host: subdomain.example.local
2024-09-06T10:30:33.576446121Z Rewriting routes for hostname: subdomain.example.local
2024-09-06T10:30:33.576449969Z Rewriting /folderb folder to root
2024-09-06T10:30:33.581293825Z Sending response with status 200 /__nuxt_error?url=https:%2F%2Fsubdomain.example.local%2F&statusCode=404&statusMessage=Cannot+find+any+route+matching+https:%2F%2Fsubdomain.example.local%2F.&message=Cannot+find+any+route+matching+https:%2F%2Fsubdomain.example.local%2F.&stack /__nuxt_error?url=https:%2F%2Fsubdomain.example.local%2F&statusCode=404&statusMessage=Cannot+find+any+route+matching+https:%2F%2Fsubdomain.example.local%2F.&message=Cannot+find+any+route+matching+https:%2F%2Fsubdomain.example.local%2F.&stack ["host","example.local","user-agent","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36","accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","accept-encoding","gzip, deflate, br, zstd","accept-language","en-US,en;q=0.9,nl;q=0.8","cache-control","no-cache","cookie","i18n_redirected=nl","pragma","no-cache","priority","u=0, i","sec-ch-ua","\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"","sec-ch-ua-mobile","?0","sec-ch-ua-platform","\"Linux\"","sec-fetch-dest","document","sec-fetch-mode","navigate","sec-fetch-site","none","sec-fetch-user","?1","upgrade-insecure-requests","1","x-forwarded-for","172.19.0.1","x-forwarded-host","example.local","x-forwarded-proto","https","x-requested-host","subdomain.example.local","x-nuxt-error","true"] {"vary":"Accept-Encoding","content-type":"text/html;charset=utf-8","x-powered-by":"Nuxt"}

At this point, I am wondering what I could do and given @adamdehaven worked on this I was wondering if you managed to make it work or if more work is required in Nitro / Nuxt.


Small update I managed to debug my way to the following error: onError H3Error: Cannot find any route matching https://example.domain.local/.


Update

It all boils down to the following, the node req url property with the main domain is / while for subdomains contains the entire URL. I have no clue why this happens, though. But is part of the entire node request object from the start, it seems. I am now looking at ways to overwrite this through one of the available hooks.

get path() {
    return this._path || this.node.req.url || '/'
  }

2024-09-09 - Update

Based on another change suggested for H3 about sanitizing the URL (https://github.com/unjs/h3/pull/765) I forked the repo and made my own change to fix this and resolve it:

https://github.com/nschipperbrainsmith/h3-tentant-fix/commit/0a7da1f18e95c8a303e1512fd7a555a38ddd9e8c

This allows me to do the following in Nuxt:

const urlRegex = /^(?:(?:https|http)\:\/\/)?(?:[a-z_-]*\.)?(?:[a-z_-]*\.)(?:local|site|nl)(.*)$/
  nitroApp.hooks.hook('request', (event) => {
    console.debug(
      'Incoming request for URL',
      event.node.req.url,
      event.node.req.originalUrl,
      JSON.stringify(event.node.req.rawHeaders)
    )

    const url = event.node.req.url || ''
    const matches = url.match(urlRegex)
    if (matches) {
      const match = matches[1] || '/'
      console.debug('URL matches regex pattern, setting path to', match)
      event.node.req.url = match
      event._path = match
    }
  })
})