14

I need to get the full base URL (e.g. http://localhost:5000 or https://productionserver.com) of my Angular 2 app so that I can pass it along to a 3rd party service in the context of the app. The app's location varies depending on whether it is development, various staging/testing environments, or production, and I'd like to detect it dynamically so I don't need to maintain a hard-coded list.

A similar question has been posted in the past, but the answers (i.e. use some version of the window.location.hostname or window.location.origin property) only work when the angular2 app is being rendered by the browser.

I would like my app to work with Angular Universal, which means it needs to be rendered on the server-side where there is no access to DOM objects like window.location.

Any ideas on how to accomplish this? For reference, using asp.net core as the back-end (using the default dotnet new angular template).

Ismoil Shokirov
  • 2,213
  • 2
  • 16
  • 32
Andrew Stegmaier
  • 3,429
  • 2
  • 14
  • 26
  • There is ORIGIN_URL provider in Universal, https://github.com/angular/universal/blob/2.x/modules/platform-node/tokens.ts#L6 . I would suggest to check if it works. Any way, a good habit is to provide environment variable/provider that contains host name. The host can be different for API calls, and you may end up doing the same thing sooner or later. – Estus Flask Apr 14 '17 at 01:13
  • Try to inject it like `@Inject(ORIGIN_URL) @Optional() origin` and debug what it equals to on server side. – Estus Flask Apr 14 '17 at 01:29
  • Thanks for the tip! I'm a bit new to Angular, so forgive me if this is obvious, but do you know how I could check if ORIGIN_URL would give me what I'm looking for? I tried asking for it the constructor of my Component like this: constructor(@Inject(ORIGIN_URL) private _origin) But I get the error “Can’t resolve all parameters for HomeComponent”. I tried adding it to the Providers array of my ngModule decorator, but that didn’t seem to change the error either. Maybe there’s some syntax details that I’m ignorant of? – Andrew Stegmaier Apr 14 '17 at 01:33
  • It should be marked as optional because it doesn't exist on client side. – Estus Flask Apr 14 '17 at 01:53
  • Thanks! Looks like it has the value on the server side. I figured that out by just rendering it to the template wrapped in {{}}, which isn't touched by the client. Strangely, though even though my constructor looks like constructor(@Optional() @Inject(ORIGIN_URL) origin), the client keeps complaining with the same error ("can't resolve all parameters"). – Andrew Stegmaier Apr 14 '17 at 02:07
  • Not sure how this is possible, there usually shouldn't be *can't resolve all parameters* error at all for @Inject, probably the problem is somewhere else, or this depends on Angular version. Any way, having environment variable/service that contains hostname looks like more solid approach. Universal is deprecated. I don't think that there's ORIGIN_URL in Angular 4. – Estus Flask Apr 14 '17 at 13:33
  • Actually, I tried it out on Angular 4, and it looks like the ORIGIN_URL is provided both on the server _and_ the client. Magic! Guess I now have a reason to upgrade. Thanks again for all your help! – Andrew Stegmaier Apr 14 '17 at 18:44
  • That's quite strange because I don't see 'ORIGIN_URL' anywhere in Angular 4 codebase. Where are you importing it from? Consider providing an answer if you came up with a solution. Self-answers are welcome on SO. – Estus Flask Apr 14 '17 at 18:51
  • On further inspection, it seems that the Angular 4 template I was using included this line: `export const ORIGIN_URL = new InjectionToken('ORIGIN_URL')`; which might have been providing the magic. It looks like InjectionToken was added in Angular4. I'll try to add a complete solution/explanation once I get it all sorted out. – Andrew Stegmaier Apr 14 '17 at 19:36
  • The Angular 4 template that I was using that caused me to think that Angular4 had solved the problem is here: [link](https://github.com/MarkPieszak/aspnetcore-angular2-universal). I'm drilling into it now to see how it works. When I figure it out, I'll post the solution. Thanks again for all your help! – Andrew Stegmaier Apr 14 '17 at 19:45
  • You're welcome. Yes, it makes sense. Thanks for the hint on InjectionToken, looks interesting, it is new to A4. Just as I thought, it looks like it sets ORIGIN_URL provider to something like `location.origin` for browser module and `'http://' + req.headers.host` for server module. Not sure about protocol and host name detection in Node - they may be screwed up any way if the server is behind proxy. That's why specifying host explicitly looks like safer bet to me. – Estus Flask Apr 14 '17 at 20:34

5 Answers5

24

I have bit of working code with angular 5 and angular universal

in server.ts replace this

app.engine('html', (_, options, callback) => {
    let engine = ngExpressEngine({
        bootstrap: AppServerModuleNgFactory,
        providers: [
            { provide: 'request', useFactory: () => options.req, deps: [] },
            provideModuleMap(LAZY_MODULE_MAP)
        ]
    });
    engine(_, options, callback);
});

and in Angular side you can get host with below code

export class AppComponent {
    constructor(
        private injector: Injector,
        @Inject(PLATFORM_ID) private platformId: Object
    ) {
        console.log('hi, we\'re here!');
        if (isPlatformServer(this.platformId)) {
            let req = this.injector.get('request');
            console.log("locales from crawlers: " + req.headers["accept-language"]);
            console.log("host: " + req.get('host'));
            console.log("headers: ", req.headers);
        } else {
            console.log('we\'re rendering from the browser, there is no request object.');
        }
    }
}
chintan adatiya
  • 1,233
  • 14
  • 17
  • 2
    When i run it in my local it works fine but on the server it gives an error of No provider request! . Is there a solution for this? I am working on Angular 4 – twitch Mar 06 '18 at 14:40
  • there should be some problem in your server.ts – chintan adatiya Mar 14 '18 at 08:28
  • Very very nice. Works for me. – Matt Feb 22 '19 at 07:00
  • 3
    Looks like this could work: https://github.com/DmitriyIvchenko/angular/blob/10a8e5056b9c66aa7006b02e45c13fd2bae24350/project/application/src/app/modules/core/services/http-request-data/http-request-data.service.ts – Kim T Apr 04 '19 at 20:49
5

Now I'm using the server.ts ngExpressEngine:

import { ngExpressEngine } from '@nguniversal/express-engine';

const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');

    const {provideModuleMap} = require('@nguniversal/module-map-ngfactory-loader');

    app.engine('html', ngExpressEngine({
        bootstrap: AppServerModuleNgFactory,
        providers: [
            provideModuleMap(LAZY_MODULE_MAP)
        ]
    }));

And after that I can use in location.service.ts:

constructor(@Optional() @Inject(REQUEST) private request: any,
            @Optional() @Inject(RESPONSE) private response: any,
            @Inject(PLATFORM_ID) private platformId: Object)
{
  if (isPlatformServer(this.platformId))
  {
    console.log(this.request.get('host’)); // host on the server
  } else
  {
    console.log(document.location.hostname); // host on the browser
  }
}
Ivan Kalashnik
  • 411
  • 7
  • 12
  • 1
    can you please elaborate more how to use it? I want to access host with subdomain in my angular code – chintan adatiya Nov 20 '17 at 12:09
  • 3
    This doesn't work in my case. `this.request.get('host')` return always 127.0.0.1:4000 – kris_IV Jan 29 '18 at 19:23
  • Example here: https://github.com/DmitriyIvchenko/angular/blob/10a8e5056b9c66aa7006b02e45c13fd2bae24350/project/application/src/app/modules/core/services/http-request-data/http-request-data.service.ts – Kim T Apr 04 '19 at 20:50
2

Angular 14 with Universal. I had to use below code to make it work.

server.ts


  server.engine('html',  (_, options : any, callback) => {
    let engine = ngExpressEngine({
      bootstrap: AppServerModule,
      providers: [ { provide: 'host', useFactory: () => options.req.get('host') } ]
    });
  
    engine(_, options, callback)
  })

And in app.component.ts

  constructor(
    @Inject(DOCUMENT) private document: Document,
    private injector: Injector,
    @Inject(PLATFORM_ID) private platformId: Object,
  ) 
{
    if (isPlatformServer(this.platformId)) {
      console.log(this.injector.get('host'))
    } else {
      console.log('document: ', document.location.hostname);
    }
}

Santosh
  • 3,477
  • 5
  • 37
  • 75
1

You’ll find that all content coming from Http requests won’t be pre-rendered: it’s because Universal needs absolute URLs.

As your development and production server won’t have the same URL, it’s quite painful to manage it on your own.

My solution to automate this : using the new HttpClient interceptor feature of Angular 4.3, combined with the Express engine.

The interceptor catches all requests when in server context to prepend the full URL.

import { Injectable, Inject, Optional } from '@angular/core';
 import { HttpInterceptor, HttpHandler, HttpRequest } from'@angular/common/http';
 @Injectable()
 export class UniversalInterceptor implements HttpInterceptor {
  constructor(@Optional() @Inject('serverUrl') protected serverUrl: string) {}
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const serverReq = !this.serverUrl ? req : req.clone({
      url: ``${this.serverUrl}${req.url}``
    });
    return next.handle(serverReq);
  }
}

Then provide it in your AppServerModule :

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { UniversalInterceptor } from './universal.interceptor';
@NgModule({
  imports: [
    AppModule,
    ServerModule
  ],
  providers: [{
    provide: HTTP_INTERCEPTORS,
    useClass: UniversalInterceptor,
    /* Multi is important or you will delete all the other interceptors */
    multi: true
  }],
  bootstrap: [AppComponent]
})
export class AppServerModule {}

Now you can use Express engine to pass the full URL to Angular, just update your server.js :

 function angularRouter(req, res) { 
  res.render('index', {
    req,
    res,
    providers: [{
      provide: 'serverUrl',
      useValue: `${req.protocol}://${req.get('host')}`
    }]
  });
}
Kooldandy
  • 545
  • 4
  • 15
0

Thanks to help from estus, I managed to hack together something that works.

It looks like most Angular Universal templates actually have the server passing a zone parameter called "originUrl", which gets used by the server rendering methods to provide a ORIGIN_URL token that can be injected in other methods. I couldn't find any documentation about this, but you can see an example here.

So if you write something like this...

export function getBaseUrl() {
    if (Zone.current.get("originUrl")) {
        return Zone.current.get('originUrl');
    } else if (location) {
        return location.origin;
    } else {
        return 'something went wrong!';
    }
}

You should be able to get the full origin URL on both the server and the client.

Andrew Stegmaier
  • 3,429
  • 2
  • 14
  • 26
  • 2
    `Zone.current.get("originUrl")` did not work for me in Angular 5. – xuhcc Jan 14 '18 at 11:51
  • @xuhcc - did you found a solution? – kris_IV Jan 29 '18 at 14:33
  • @kris_IV - yes, I added the `request` provider as @chintan adatiya suggested. – xuhcc Jan 29 '18 at 17:45
  • @xuhcc solution doesn't work for me - I get host: 127.0.0.1:4000 at first request but I need domain ;/ – kris_IV Jan 29 '18 at 18:33
  • @kris_IV this may happen when your angular universal app is running behind a reverse proxy. Try to check `X-Forwarded-For` and `X-Real-IP` headers. – xuhcc Jan 30 '18 at 08:44
  • Any of this X-Forwarded-For and X-Real-IP doens't return proper domain, but I found a solution and wrote a post on StackOverflow for future generations at https://stackoverflow.com/questions/48509019/get-domain-from-angular-5-universal-always-return-127-0-0-1 – kris_IV Jan 30 '18 at 08:46