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

Support canonical URL #2052

Open
pi0 opened this issue Jan 5, 2024 · 6 comments
Open

Support canonical URL #2052

pi0 opened this issue Jan 5, 2024 · 6 comments
Labels
discussion enhancement New feature or request

Comments

@pi0
Copy link
Member

pi0 commented Jan 5, 2024

ref nuxt/nuxt#24813

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

  • In SSG, we cannot access the desired URL
  • In runtime, sometimes, we need a canonical URL different from the incoming request
  • In cached event handlers, we don't have access to incoming headers without opting more headers to varies (which can also lead to cache leaks)
  • When we require a full URL for generating content there is no one standard way to depend on
  • With fetch(/path) we need a default host (instead of assuming localhost)

Proposal:

  • Support app.canonicalURL in runtimeConfig for env support + app config for dynamic behavior support (prefer app)
  • Use canonical URL as default fallback for useRequestURL()
  • Introduce new useCanonicalURL() (suggested by @atinux) that falls back to useRequestURL() if the user hasn't configured a canonical URL and fails for SSG if not provided
@adamdehaven
Copy link
Member

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

@pi0
Copy link
Member Author

pi0 commented Apr 22, 2024

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

  • If there is one dynamic canonical per instance, via runtime config
  • 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
  • (consider solution discussed in this issue, is mainly to cover situations when we don't have full dynamic possibilities such as build-time determination. Some limits might need to be solved in higher-order solutions such as in Nuxt that have control over client

@adamdehaven
Copy link
Member

  • 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:

  • When on Cloudflare with a custom domain, it sometimes returns the commit subdomain on the Nitro server instead of the CNAME custom domain
  • Other times, it returns localhost

There doesn't seem to be a consistent way

@pi0
Copy link
Member Author

pi0 commented Apr 22, 2024

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
Copy link
Member

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: #2388

@nschipperbrainsmith
Copy link

nschipperbrainsmith commented Sep 6, 2024

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 (unjs/h3#765) I forked the repo and made my own change to fix this and resolve it:

nschipperbrainsmith/h3-tentant-fix@0a7da1f

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
    }
  })
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants