#Proper way for catch all domain routing?
43 messages · Page 1 of 1 (latest)
You're using a proxy, so you should set up the TrustProxies middleware to correctly identify proxies. You'd probably also have to set up the TrustHosts middleware
@urban sandal TrustProxies seems to be enabled by default, anything else you would need to setup? Forwarded for is in the headers and its showing the custom domain.
@delicate mason caddy listens for a hostname and will generate an SSL certificate for it, it then proxies to team.mydomain.com to fetch the data however I can't listen on team.mydomain.com route since it actually comes in as mydomain.com (which is weird)
"HTTP_HOST" => "team.mydomain.ai"
But trying to get the route on team
Route::domain('team.mydomain.ai')->group(function () {
Route::get('/', function ($domain) {
return 'Hello from custom domain: ' . $domain;
});
});
Will not fire
While debugging that is the only route in my web.php trying to hook into the proxy.
If you set the proxy up correctly and the middleware is enabled, Laravel would correctly identify the host. No need to mess with PHP globals, Request::host() would return the correct value then, routes would correctly identify the host
@urban sandal request()->getHost() is returning the custom domain that the user setup. But I need to send all custom domain traffic to a specific route group.
So how have you defined this route? It would need to have more priority than a route without a domain
@urban sandal orignally I tried this
Route::domain('{domainOrSubdomain}')->middleware('setTenant')->group(function () {
Route::name('blog.')->group(function () {
// Route::get('/', [\App\Http\Controllers\CategoryController::class, 'index'])->name('home');
Route::get('/', function () {
return 'Hello from blog.domain.com!';
});
Route::resource('category', \App\Http\Controllers\CategoryController::class)->parameters(['post' => 'post']);
Route::resource('category.post', \App\Http\Controllers\PostController::class)->parameters(['post' => 'post']);
Route::resource('tag', \App\Http\Controllers\TagController::class)->parameters(['tag' => 'slug']);
Route::get('/t/{post}', function ($id) {
$post = Post::findOrFail($id);
return redirect()->to(route('category.post.show', [$post->category->slug, $post->slug]));
})->name('post.short_link');
});
});
setTenant will abort 404 if custom domain does not exist in our database
And put this near the top of the routes. then my
Route::domain(config('social.domain'))->group(function () {
Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
});
So what's the issue exactly? There's so much going on here it's impossible to pinpoint what the issue is. Trim it down to a simple test, then go from there
this route
Route::domain('{domainOrSubdomain}')-
Never gets executed, I figured its because it needs to catch on the subdomain not the "custom domain".
How do you know it "never gets called"? You have a middleware on it, maybe that screws things over. Is it present in your route list? What does happen?
my middleware contains logging and also dd at the top when I got frustrated
public function handle(Request $request, Closure $next): Response
{
dd($domain = $request->route('domain'));
$customDomain = $request->header('user-custom-domain');
if ($customDomain) {
Log::debug('SetTenantMiddlewareDomain', [$customDomain]);
$customDomainRecord = CustomDomain::query()
->where('domain', $customDomain)
->first();
if ($customDomainRecord) {
Log::debug('SetTenantMiddlewareRecord', [$customDomainRecord->team->id]);
request()->session()->put('tenant', $customDomainRecord->team->id);
return $next($request);
}
}
$domainParts = explode('.', $request->getHost());
if (count($domainParts) === 3) {
Log::debug('SetTenantMiddlewareParts', [$domainParts]);
$subdomain = $domainParts[0];
$mainDomain = $domainParts[1] . '.' . $domainParts[2];
if ($mainDomain === parse_url(config('app.url'), PHP_URL_HOST)) {
$team = Team::find($subdomain);
if ($team) {
Log::debug('SetTenantMiddlewareTeam', [$team->id]);
request()->session()->put('tenant', $team->id);
return $next($request);
}
}
}
// If we reach here, we didn't find a valid custom domain or subdomain.
// Abort the request so Laravel continues to the next route.
Log::debug('SetTenantMiddlewareAbort');
abort(503, 'No tenant found.');
}
I also tried it without the middleware to see if I could just get the "hello from..." but that does not work either
everything is on vapor so it takes a bit to get this testing properly with the deployments
So... testing on production.. Why not make it work locally first, and then worry about deploying? That'd be much easier to debug and help you
caddy can't proxy to localhost, I was hoping i could trigger the route on team.mydomain.com initially which is why i moved testing to production
caddy can't proxy to localhost
Since when?
The very first example in the docs is from localhost to localhost; https://caddyserver.com/docs/quick-starts/reverse-proxy#command-line
@urban sandal i'll give it a shot, this would speed the testing up for sure I figured it had to be live since I was hardcoding the domain in but ill just use hosts to bypass this
@urban sandal ok got it setup locally which will save loads of time debugging this. Not to sure where to start though to get the catch all route to check if its an authorized custom domain or not though
Route::get('/debug-route', function() {
dd(request(), session(), request()->getHost());
});
//
Route::domain('blog.custom.test')->group(function () {
Route::get('/', function ($domain) {
return 'Hello from custom domain: ' . $domain;
});
});
@urban sandal the first route will trigger if I goto debug-route but without it goes to a 404
getHost shows blog.custom.test
So what does your route:list look like?
GET|HEAD blog.custom.test/ ...............................................................
So there's only one route..?
Also, you don't have to tag me 20 times each time you send a message, it's kind of annoying to get multiple pings from one person wanting a bit of attention 🥲
sorry about that
ahhh wait a second, FF is not using the hosts file chrome seems to be,
ok so chrome will allow this
Route::domain('blog.custom.test')->group(function () {
Route::get('/', function () {
return 'Hello from custom domain: ';
});
});
That works but
Route::domain('{domain}')->group(function () {
Route::get('/', function ($domain) {
return 'Hello from custom domain: '.$domain;
});
});
as a wildcard does not
but if I do
Route::domain('{domain}.custom.test')->group(function () {
Route::get('/', function ($domain) {
return 'Hello from custom domain: '.$domain;
});
});
that will fire as well, Hello from custom domain: blog
I think that wildcard doesn't work, because Laravel by default doesn't allow dots in a variable, so you'd want something like
Route::domain('{domain}')
->where('domain', '.*')
->group(function () {
//
});
(adjust accordingly ofc)
that seemed to do the trick perfectly, had to move the where after the group otherwise it wants an array
Route::domain('{domain}')
->group(function () {
Route::get('/', function ($domain) {
return 'Hello from custom domain: '.$domain;
})->where('domain', '.*');
});
Thank you sir, been stumped for a couple days now. Deploying to vapor to test as well was painful as well so 2 birds on this one.
hmm the domain where actually does not make sense on the get itself i'll have to play around with it on pre-group
Then you're only applying the where on a single route, you'd want it on the group, otherwise you have to repeat that for every route
https://laravel.com/docs/10.x/routing#parameters-regular-expression-constraints
Should be possible on a group afaik. Otherwise you could opt to do it in globally
Middleware and where conditions are merged while names and prefixes are appended. Namespace delimiters and slashes in URI prefixes are automatically added where appropriate.
figured you could then do
Route::domain('{domain}')
->where(['domain' => '.*'])
->group(function () {
Route::get('/', function ($domain) {
return 'Hello from custom domain: '.$domain;
});
});
which works, it seems this firefox issue is weird. Chrome is fine
No clue what that has to do with a browser tbh
dev in chrome for this part
I am guessing its some sort of privacy dns caching of some sort
thanks again for the help robert
@urban sandal one last thing and I think I got this handled
Route::domain('{domain}')
->middleware('setTenant')
->where(['domain' => '.*'])
->group(function () {
Route::name('blog.')->group(function () {
Route::get('/', [\App\Http\Controllers\CategoryController::class, 'index'])->name('home');
Route::resource('category', \App\Http\Controllers\CategoryController::class)->parameters(['post' => 'post']);
Route::resource('category.post', \App\Http\Controllers\PostController::class)->parameters(['post' => 'post']);
Route::resource('tag', \App\Http\Controllers\TagController::class)->parameters(['tag' => 'slug']);
Route::get('/t/{post}', function ($id) {
$post = Post::findOrFail($id);
return redirect()->to(route('category.post.show', [$post->category->slug, $post->slug]));
})->name('post.short_link');
});
});
is working perfectly setting the tenant etc however when the middleware fails I want it to keep looking for routes for example this one here:
Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
});
the middleware is currently returning abort(404) when the custom domain is not found, is that not the proper way to exit out of a middleware and continue on if that group is not authorized and move onto the next group ?
<?php
namespace App\Http\Middleware;
use App\Models\CustomDomain;
use App\Models\Team;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class SetTenantMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$customDomain = $request->host();
if ($customDomain) {
Log::debug('SetTenantMiddlewareDomain', [$customDomain]);
$customDomainRecord = CustomDomain::query()
->where('domain', $customDomain)
->first();
if ($customDomainRecord) {
Log::debug('SetTenantMiddlewareRecord', [$customDomainRecord->team->id]);
request()->session()->put('tenant', $customDomainRecord->team->id);
return $next($request);
}
}
$domainParts = explode('.', $request->getHost());
if (count($domainParts) === 3) {
Log::debug('SetTenantMiddlewareParts', [$domainParts]);
$subdomain = $domainParts[0];
$mainDomain = $domainParts[1] . '.' . $domainParts[2];
if ($mainDomain === parse_url(config('app.url'), PHP_URL_HOST)) {
$team = Team::find($subdomain);
if ($team) {
Log::debug('SetTenantMiddlewareTeam', [$team->id]);
request()->session()->put('tenant', $team->id);
return $next($request);
}
}
}
// If we reach here, we didn't find a valid custom domain or subdomain.
// Abort the request so Laravel continues to the next route.
Log::debug('SetTenantMiddlewareAbort');
abort(404, 'No tenant found.');
}
}
Why would you want to keep looking for routes? The router matches a route, then that's it really. It doesn't exist, so show an error page, or maybe redirect to one of your pages
if the user has a customdomain of say blog.theirdomain.com ideally I want to show their blog,
if the user does teamid.mydomain.com then i want to show their blog as well
This is handled by the middleware
However if the host is mydomain.com then I want to show our homepage and allow the dashboard/admin interface etc.
Talking this out I guess i could do where domain === ourdomain on the routes file and put that above the domain wildcard.