Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.

Angular Server Side Rendering issue with ORIGIN_URL #989

Closed
AmrineA opened this issue May 30, 2017 · 19 comments
Closed

Angular Server Side Rendering issue with ORIGIN_URL #989

AmrineA opened this issue May 30, 2017 · 19 comments

Comments

@AmrineA
Copy link

AmrineA commented May 30, 2017

I noticed in the out of the box templates, I'm able to go to the Fetch Data page and refresh the browser and it properly does the call out to the server as you would expect. However, in my application, I get a completely different result.

Exception: Call to Node module failed with error: Error: URLs requested via Http on the server must be absolute. URL: undefined/api/Cases/1

I'm importing ORIGIN_URL into the method that needs the value, but it is coming back as undefined for some reason. I've also got it in the boot-server.ts file:

import 'reflect-metadata';
import 'zone.js';
import 'rxjs/add/operator/first';
import { enableProdMode, ApplicationRef, NgZone, ValueProvider } from '@angular/core';
import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
import { AppModule } from './app/app.module.server';

enableProdMode();

export default createServerRenderer(params => {
    const providers = [
        { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
        { provide: 'ORIGIN_URL', useValue: params.origin }
    ];

    return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
        const appRef = moduleRef.injector.get(ApplicationRef);
        const state = moduleRef.injector.get(PlatformState);
        const zone = moduleRef.injector.get(NgZone);

        return new Promise<RenderResult>((resolve, reject) => {
            zone.onError.subscribe(errorInfo => reject(errorInfo));
            appRef.isStable.first(isStable => isStable).subscribe(() => {
                // Because 'onStable' fires before 'onError', we have to delay slightly before
                // completing the request in case there's an error to report
                setImmediate(() => {
                    resolve({
                        html: state.renderToString()
                    });
                    moduleRef.destroy();
                });
            });
        });
    });
});

Where does params.origin get its value from? Does that need set somewhere that I'm missing?

@MarkPieszak
Copy link
Contributor

You need to @Inject() it and prepend your Http calls with ORIGIN_URL as servers need absolute urls. There should be an example of injecting it in one of the components.

@AmrineA
Copy link
Author

AmrineA commented May 30, 2017

Hi @MarkPieszak. I'm pretty sure I've got it in place the correct way.

This is how I'm doing it:

get(id: number, @Inject('ORIGIN_URL') originUrl: string): Observable<Case> {
        return this.httpService.get<Case>(`${originUrl}/api/Cases/${id}`);
    }

originUrl ends up being undefined, which is why the path ends up being

undefined/api/Cases/1

@ADefWebserver
Copy link

Perhaps try opening the file at Views/Shared/_Layout.cshtml and changing the base href to the following:

<base href="~/" />

See:
#961 (comment)

@AmrineA
Copy link
Author

AmrineA commented May 30, 2017

@ADefWebserver Thanks for the suggestion. Just tried it, no luck. Same issue.

@MarkPieszak
Copy link
Contributor

@AmrineA
Copy link
Author

AmrineA commented May 30, 2017

@MarkPieszak Yes, it works client side. The problem is when I go to a specific page deeper in the system and then try to make it do a server side refresh which involves making a server side API call. Do I need a line similar to that in the app.module.server.ts file? I see there are providers specified in boot-server.ts.

@MarkPieszak
Copy link
Contributor

MarkPieszak commented May 30, 2017

Are you trying to use @Inject() inside of a method?

It looks like you have it in some get() method (also note get is typically a restricted names, as get/set are getters/setters in TS.

    get (id: number, @Inject('ORIGIN_URL') originUrl: string): Observable<Case> {
        return this.httpService.get<Case>(`${originUrl}/api/Cases/${id}`);
    }

This needs to be in the constructor of whatever service/class/component you have it in.

   constructor( /* ...whatever-other-dependencies... */ , @Inject('ORIGIN_URL') originUrl: string ) { }

    // now when you do your get method, you can grab it by this.originUrl
    get (id: number): Observable<Case> {
        return this.httpService.get<Case>(`${ this.originUrl }/api/Cases/${id}`);
    }

@AmrineA
Copy link
Author

AmrineA commented May 30, 2017

@MarkPieszak I tried it in a constructor originally. When I put it in the constructor, I was unable to access it in the method. It complained about it not being accessible. That's when I tried the method. I did change it back to the constructor, but I had to set a private variable in the class using the constructor to a private variable inside of the class.

All of this is being done inside of a service, not a component. I'm not sure if that makes a difference.

@MarkPieszak
Copy link
Contributor

It's accessible in the method via this. (so, this.originUrl).

One thing I just realized I forgot, and this is probably why it wasn't accessible in your method, make sure you add a private/public accessor infront of the variable name for it. That should do the trick.

    constructor( @Inject('ORIGIN_URL') private originUrl: string ) { } // note the "private"

@AmrineA
Copy link
Author

AmrineA commented May 30, 2017

@MarkPieszak It's working now! I honestly am confused as to what is different now versus what I had tried in the past, but I'm glad it's working. Thanks for your help.

One other question regarding SSR (and I'll create a separate issue or post on StackOverflow if that's best): our API endpoints are decorated with the Authorize attribute, and we store the token in sessionStorage. Of course, sessionStorage isn't available server side so when SSR occurs, it shows our login page for a few seconds before properly authenticating the user on the client side again with the token and then sending them back to the original page.

@AmrineA AmrineA closed this as completed May 30, 2017
@MarkPieszak
Copy link
Contributor

That's a dependency issue issue you'll be dealing with, similar to how you did this here with ORIGIN_URL, you'll have to make abstract out the concept of "Storage" into an abstract class, that you can dependency inject into the client, doing a useClass: BrowserStorage, and the same but different class name and implementation for the server, where it doesn't look in a Storage, by just in some object you passed during the SSR.

I'm hopefully releasing some articles in the next week or two on some of these deeper SSR & DI problems as they aren't concepts everyone is as used to!

@AmrineA
Copy link
Author

AmrineA commented May 31, 2017

Thanks @MarkPieszak. Looking forward to reading them. I understand the concept of what needs to happen. I was doing some more research and see how others have worked through a similar thing, but I'm not sure if there is some sort of best practice at this point, or if it's too early for there to even be a best practice. 😄

@astegmaier
Copy link

astegmaier commented Jun 6, 2017

I had the same question as @AmrineA about the best/recommended way to pass auth tokens to the SSR context. What you wrote makes sense, but I had a follow-up question about this statement:

"the same but different class name and implementation for the server, where it doesn't look in a Storage, by just in some object you passed during the SSR."

What is the best way to pass the "some object" between the client and the server? I'd like an already-authenticated user to be able to hit "F5" and be able to see the prerendered page with authenticated content display smoothly with no flashing. Do I have to store the auth tokens in cookies to enable this? And if so, should my server side api controllers just be using cookie authorization to begin with (instead of bearer auth)? That would be suboptimal in my case because ideally I'd like my server APIs to work smoothly for mobile clients as well, and bearer auth seems preferable in that context.

@MarkPieszak - I can't wait to read the articles you mentioned above. Do you think they will address this question?

@AmrineA
Copy link
Author

AmrineA commented Jun 7, 2017

@astegmaier Using cookies is the only way that I've seen used at this point. It's probably the most common thing that traverses both client and server. We too use bearer tokens, which means potentially storing the token in two different places in order for it to work. Not really an ideal scenario, but might be the only option at this point.

@MarkPieszak
Copy link
Contributor

@astegmaier I'm hoping to tap into many common but advanced issues with SSR, these are cross-framework as well, as everyone has the same problems.

Hopefully release at least part 1 sometime soon.

@BillyQin
Copy link

@MarkPieszak I use
providers: [
{ provide: 'ORIGIN_URL', useValue: location.origin }
]
but location is browser object , how to use location in node ? thanks

@OskarKlintrot
Copy link

@BillyQin https://github.com/aspnet/JavaScriptServices/blob/dev/templates/AngularSpa/ClientApp/boot-server.ts#L14:

    const providers = [
        { provide: 'ORIGIN_URL', useValue: params.origin }
    ];

@astegmaier
Copy link

@MarkPieszak - did you have a chance to write that article? I wasn't sure where to look for it--medium? twitter?

(I'm still spinning my wheels trying to create an optimal auth system that works great both client- and server-side, supports social auth and local accounts together, and (ideally) doesn't involve rolling the whole thing myself. Learning the frameworks while trying to forge a new path is tough, and I was hoping to learn from the masters :-) I appreciate all the work you've done to help us newbies out!)

@Alexx999
Copy link

Alexx999 commented Oct 2, 2017

When using latest Angular SPATemplate it's BASE_URL now instead of ORIGIN_URL

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants