Open Reuvenc opened 7 years ago
Hi this is a problem of xhr2
module.
I dont have solution, but i have workaround that works nice :).
At the beginning of your main-server.ts file add these lines
import * as xhr2 from 'xhr2';
//HACK - enables setting cookie header
xhr2.prototype._restrictedHeaders.cookie = false;
This allows you to send cookie header. Not clean solution, but it looks like only one so far.
Hope this helps :)
Even retrieving cookie does not work on Server Side. I was able to set cookies, but couldn't retrieve the same.
Zone.current.get('cookie')
The above used to work in angular2 universal starters but not in angular4
Here you can see how to use cookies.
https://github.com/kukjevov/ng-universal-demo/tree/mine
I have CookiesService
, that has constructor with parameter looking like this @Optional() @Inject(SERVER_COOKIE_HEADER) private _serverCookies: string
Also i have this code that is in method that is used for retrieving cookies this code
if(isBlank(this._serverCookies))
{
result = regexp.exec(document.cookie);
}
else
{
result = regexp.exec(this._serverCookies);
}
in another file i have
/**
* Token is used to transfer server cookie header
*/
export const SERVER_COOKIE_HEADER: InjectionToken<string> = new InjectionToken<string>('serverCookieHeader');
SERVER_COOKIE_HEADER
is provided only as provider for main-server.ts and value for this is obtained in server.js
with this value req.headers['cookie']
where request is NodeJs request object.
I dont want to set cookies on SSR, so i use it only for reading. It does not matter whether you have Connect, Express NodeJs server, or AspNet.Core, it works both way, because you just set cookies as string for your service for reading it. Check repository posted above, if you have any questions i can help hopefully.
@kukjevov where are the packages prefixed with @ng
coming from?
NPM couldn't find it.
Well packages prefixed @ng
are mine packages all created by me and are not open source since we are using them in our company, i can provide you snippets if you want but not whole code :(.
They are in private npm repository, i can run it, but you wont be able to run that project but it is working :) nicely.
The only thing I need here is to be able to authenticate user on server. For that I need to access the cookie on server inside angular, if you can guide me with that would be great.
I had exactly same problem, and now i know it is working, i will provide you some code tomorrow :)
Figured it out...
Using the below starter for angular4: https://github.com/FrozenPandaz/ng-universal-demo
Change your ng-universal-demo/src/app/browser-app.module.ts file to look like below (Note the Providers Array):
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module';
@NgModule({
bootstrap: [ AppComponent ],
imports: [
BrowserModule.withServerTransition({
appId: 'my-app-id'
}),
BrowserTransferStateModule,
AppModule
],
providers: [
{provide: 'REQUEST', useValue: {}}
]
})
export class BrowserAppModule {}
Now in any file where you wish to access the HTTP REQUEST Object, Inject 'REQUEST' into constructor:
constructor(@Inject('REQUEST') public request: any) {
// Below code on SSR (Node) will return cookies that come with the browser request & on browser will return {}
let _cookies = this.request.cookies || {};
}
Hope that's helpful :-)
@sangeet003 But how do you manage to receive the cookies at service side?
@dneeleman wouldn't that be a concern for the server side framework you are using?
My workaround for ng4 This will work with both components and services, I wasnt able to use @sangeet003 's solution in a service, but that works in a component.
//import these
import { Injector } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
//initialize your constructor:
constructor (private injector: Injector){}
//access the request with:
req = this.injector.get(REQUEST);
if(req.headers.cookie){
doYourOperation(req.headers.cookie);
}
To make sure things dont break on client side, use isPlatformServer(platformId) thingy before you access the request.
@pushpann
ERROR { Error: Uncaught (in promise): Error: No provider for InjectionToken REQUEST!
Error: No provider for InjectionToken REQUEST!
How do we solve this?
@916422 Are you checking if the platform is server before trying to access the request? Its only accessible on server side
@dneeleman AOT Compile ,Is the code that runs on the server!
@pushpann
I have the same problem as @916422 how can we solve this issue?
I'm trying to run my frontend with the newly released angular-cli 1.3.0rc2. It is working but I need to set some headers when my app makes server side ajax requests (origin header). Does anyone know an updated method to set headers on the http requests being done from an app using platform-server on angular 4.3 ?
Edit: Please note that this is inherently dangerous
To pass cookies from the user's browser through to HttpClient requests to your own domain, I do the following (this example also shows how to set cookies during server-side rendering in general):
in my server's main.ts
import 'zone.js/dist/zone-node';
import { platformServer, renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';
import { AppServerModule } from './app.module';
import { AppServerModuleNgFactory } from '../aot/src/server/app.module.ngfactory';
import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as cookieParser from 'cookie-parser';
import * as path from 'path';
import { router } from './routes';
import * as xhr2 from 'xhr2';
xhr2.prototype._restrictedHeaders = {};
enableProdMode();
let port = process.env.PORT || 3000;
const app = express();
app.use(express.static(path.resolve(__dirname, __dirname + '/../client')));
app.use(express.static(path.resolve(__dirname, '../../assets')));
app.use(bodyParser.json());
app.use(cookieParser());
app.use('/api', router);
app.get('*', (req, res) => {
renderModuleFactory(AppServerModuleNgFactory, {
document: require('../browser/index.html'),
url: req.url,
extraProviders: [
{
provide: 'REQUEST',
useFactory: () => req,
},
],
}).then((data) => {
res.send(data);
});
});
app.listen(port, () => {
console.log('listening on http://0.0.0.0:' + port);
});
in intercepter/default.interceptor.ts
import { Injectable } from '@angular/core';
import { PLATFORM_ID, Inject, Injector } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/observable/fromPromise';
import * as express from 'express';
@Injectable()
export class DefaultInterceptor implements HttpInterceptor {
constructor(
@Inject(PLATFORM_ID)
private platformId: string,
private injector: Injector
) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = request.clone({ withCredentials: true });
if (!isPlatformBrowser(this.platformId)) {
let req: express.Request = this.injector.get('REQUEST');
let rootDomain = req.hostname.split('.').slice(-2).join('.');
if (request.url.match(/^https?:\/\/([^/:]+)/)[1].endsWith(rootDomain)) {
let cookieString = Object.keys(req.cookies).reduce((accumulator, cookieName) => {
accumulator += cookieName + '=' + req.cookies[cookieName] + ';';
return accumulator;
}, '');
request = request.clone({
headers: request.headers.set('Cookie', cookieString)
});
}
}
return next.handle(request).do((event) => {
// console.log(request);
// console.log(event);
});
}
}
In model/whatever.service.ts
import { Injectable, NgModule } from '@angular/core';
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import 'rxjs/add/operator/toPromise';
import { DefaultInterceptor } from '../interceptor/default.interceptor';
@Injectable()
export class WhateverService {
constructor(
private http: HttpClient
) {}
public someRequest(someData: any): Promise<void> {
return this.http.post<void>('http://mydomain/api/something', someData).toPromise();
}
}
@NgModule({
imports: [
HttpClientModule,
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: DefaultInterceptor,
multi: true,
},
WhateverService,
],
})
export class WhateverModule {}
I hope this helps. I'm successfully setting cookies during server-side rendering in Angular 4.3.2
hi guyes @SystemDisc solution works great. But anyone knows if this is something that platform-server needs to handle or any work being done on it to handle cookies by default?
@asadsahi The problem is that when the user's browser sends an HTTP request to your server, the user's browser sends the cookies it has for that domain as simple key + value pairs with no extra info, and when HTTP requests are made during SSR it becomes difficult to know whether the cookies the user's browser sent should also be passed on to wherever the request we're making is going - this would be especially difficult to figure out for subdomains (i.e. the user's browser sends its cookies to example.com (where Node is sitting) and a request in your app is going to subdomain.example.com - we have no way of knowing if any of the cookies we received should also go to subdomain.example.com).
Be very careful with my solution. If you're not, you may end up sending sensitive information out to third parties.
Edit: Also note that if your root domain is example.com, this statement in my code request.url.match(/^https?:\/\/([^/:]+)/)[1].endsWith(rootDomain)
will match anything.example.com, but also anythingexample.com.
@SystemDisc What happens in case that authentication is facebook login from the client side In this case client will exchange the facebook code with a jwt token . So for the jwt to work I have to store it in cookies and pass it the way you provided above? Is is safe ?
@DionisisKav No, my "solution" is inherently dangerous. Use your best judgment.
@SystemDisc The code you used below is to pass the cookies from request in angular universal . But if the api responds with a new token (like JWT refresh token) how you will pass this token from server to angular and save it in a cookie ?
Thanks @ZornCo! Your solution helped me a lot! I'm being able to pass cookies correctly now to server requests!
Hi,
It looks like webpack bundles the xhr2 library in the server.js file after the server.ts or main.server.ts files are defined, so the trick to amend "xhr2.prototype._restrictedHeaders = {};" is overwritten by the definition of the xhr2 library.
Is there anything I could do to avoid the "xhr2.prototype._restrictedHeaders = {};" is overwritten? Maybe I could change something that in the webpack.xerver.config.js file?
Many thanks
@jem890 @angular/platform-server
is using its own packaged version of xhr2. The workaround I'm using is to modify src/app/app.server.module
:
// activate cookie for server-side rendering
export class ServerXhr implements XhrFactory {
build(): XMLHttpRequest {
xhr2.prototype._restrictedHeaders.cookie = false;
return new xhr2.XMLHttpRequest();
}
}
@NgModule({
...
providers: [{ provide: XhrFactory, useClass: ServerXhr }],
...
})
export class AppServerModule {}
See packages/platform-server/src/http.ts for reference
@markharding Many thanks for your help. It works like a charm!
@markharding excuse my ignorance, but does that match
import * as xhr2 from 'xhr2';
xhr2.prototype._restrictedHeaders = {};
Or is it a better or more recommended way?
for anyone xhr2.prototype._restrictedHeaders
not working.. use xhr2.XMLHttpRequest.prototype._restrictedHeaders
instead
xhr2 api doc
Here's a simple HttpInterceptor example which is working for the Browser and Server to attach cookies to Http Requests.
For this example to work you must use the custom XhrFactory providers from markharding answer https://github.com/angular/angular/issues/15730#issuecomment-572992686.
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
@Optional() @Inject(REQUEST) private request: Request) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (isPlatformServer(this.platformId) && this.request && this.request.headers["cookie"]) {
req = req.clone({ headers: req.headers.set("cookie", this.request.headers["cookie"]) })
} else {
req = req.clone({ withCredentials: true })
}
return next.handle(req)
}
}
Thank you @GaetanRouzies for the workaround. Do you know how this could be achieved for Angular 17 as well? There is no 'REQUEST' InjectionToken available anymore.
My only solution right now is to disable SSR for all non static routes using this: https://github.com/nestjs/ng-universal/issues/160#issuecomment-538795780
Obviously that makes SSR almost useless :/ I hope that there will be a solution.
i am tring more than way, but i cann't solve it. Does anyone provied me any sollution for angular 17 standalone project structure?
For those wondering how to make it work in Angular 17, well the migration script should add the REQUEST
injection token like this:
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap: AppServerModule,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: distFolder,
providers: [
{ provide: APP_BASE_HREF, useValue: baseUrl },
{ provide: RESPONSE, useValue: res },
{ provide: REQUEST, useValue: req }
],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
And like @GaetanRouzies wrote, the cookies can be retrieved and forwarded easily, just dont retrieve them from request.cookies
, but from request.headers.cookie
instead.
Thanks. I solve this using this mechanism. but thanks a lot for your response.
On Sat, May 4, 2024 at 8:15 PM Guerric Phalippou @.***> wrote:
For those wondering how to make it work in Angular 17, well the migration script should add the injection token like this:
server.get('*', (req, res, next) => { const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine .render({ bootstrap: AppServerModule, documentFilePath: indexHtml, url: `${protocol}://${headers.host}${originalUrl}`, publicPath: distFolder, providers: [ { provide: APP_BASE_HREF, useValue: baseUrl }, { provide: RESPONSE, useValue: res }, { provide: REQUEST, useValue: req }], }) .then((html) => res.send(html)) .catch((err) => next(err));
});
And like @GaetanRouzies https://github.com/GaetanRouzies wrote, the cookie can be retrieved and forwarded easily, just dont retrieve them from request.cookies , but from request.headers.cookie instead.
— Reply to this email directly, view it on GitHub https://github.com/angular/angular/issues/15730#issuecomment-2094217429, or unsubscribe https://github.com/notifications/unsubscribe-auth/AXVAGGD2VZFSST3CAWNU753ZATUPFAVCNFSM4DGHOYS2U5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TEMBZGQZDCNZUGI4Q . You are receiving this because you commented.Message ID: @.***>
While the solution posted above does work for the built application, it does not work in dev mode (ng serve
) with the new builder (@angular-devkit/build-angular:dev-server
), because in this mode SSR does not use the express server from server.ts
at all, instead an internal dev server is used with no way to access the request and/or the cookies (as far as I can tell).
It would be nice if there was a way to access cookies supported by Angular itself, so that the dev server can be aware of it, too.
Edit: Finally found where the server is actually created: https://github.com/angular/angular-cli/blob/9e13a8b0c85c7f8353a061e11031bdfe4473870e/packages/angular/build/src/tools/vite/angular-memory-plugin.ts#L195-L240
It definitely looks possible to pass the request and/or the cookies to renderPage
which could provide them via the environment injector.
For those wondering how to make it work in Angular 17, well the migration script should add the
REQUEST
injection token like this:And like @GaetanRouzies wrote, the cookies can be retrieved and forwarded easily, just dont retrieve them from
request.cookies
, but fromrequest.headers.cookie
instead.
I didn't get it.
With Node v.19+, fetch can't send cookies anymore. I'm using withFetch()
on a SERVER SIDE(SSR), so all requests on the server side can't send cookies in the request header.
Yes, I can read it using DI, yes, I can pass it to SSR fetch call, but it will be ignored by the fetch(Node 19+).
How should API calls from the server side work in Angular 17+ using withFetch()
and cookies?
p.s. I know that I can patch xhr2
and don't use fetch on the server side, but Angular gives warnings about performance issues when not using fetch. Is there a solution for using withFetch()
+ cookies on Node 19+(maybe some patching like globalThis.fetch = ...
?
p.p.s. I don't need to make it work in dev mode; I want to make it work at least in prod env. on Node 19+.
@gnibeda I don't see what has changed in node 19+ that would prevent attaching of cookies to requests made from SSR side to some API?
Node 18 added native fetch
API that allows you to set request headers, including the Cookie
header. However, it's not done automatically, so that's why you need an interceptor that does it by injecting REQUEST
and modifying outgoing requests by taking request.headers.cookie
and forwarding it, as showcased by @GaetanRouzies and @Guerric-P in their comments.
It seems to me that the only outstanding issue is that this does not work in dev mode because of what @diesieben07 said about dev-server not using express server, which in turn does not provide express request object under REQUEST
DI token.
In my opinion, an ideal solution would be for Angular to provide some framework-agnostic value for the SSR request object that would work in all cases. By framework-agnostic, I mean that it does not depend on express nor whatever is used in dev mode. Some generic Request
and Response
objects that you can inject and that can be mapped to whatever is being used to handle SSR requests, be it express or anything else.
p.s. it's a bit disconcerting that the stack for handling SSR requests is completely different between dev and production modes, as there might be some issues that happen in one, but not the other. Worst case scenario - something works locally, but not in production due to differences between express and vite handling of SSR requests.
@fvoska, I mean that it is not related to Angular. Simple fetch call won't send cookies in Node 19+:
return fetch('https://test.site/rest', {
method: 'GET',
credentials: "include"
headers: {
'Content-Type': 'application/json',
'Cookie': cookie // Won't work in Node 19+, but works in 18
});
@gnibeda there is no issue with reading and sending cookies in node's native fetch methoid. In node, there is no concept of cookies per-se, like there is in browsers. From node perspective, it's just a regular HTTP header that can be received or sent.
I have created a quick example that showcases that you are able to read received cookies in node and send the cookie header in requests using fetch
.
/endpoint-1
expects a cookie, reads the cookie and makes a fetch call to /endpoint-2
, forwarding the cookie. /endpoint-2
reads the cookie that was forwarded to it and returns it. Both endpoints return the cookie header value in response body.
I have checked this on node 20, not sure about 19.
If you are having issues with this, I think there is something else wrong, as it's not a node native fetch issue.
This is getting a bit off-topic, but I wanted to make sure we can rule out that it's not a node's native fetch issue.
@fvoska, thank you. Seems that issue doesn't related to Node, because your code works fine on Node v20.14.
Any ideas why, using withFetch()
on server side, prevents cookies from sending?
I have following code in interceptor:
req.clone({withCredentials: true, headers: new HttpHeaders({cookie: ssrCookies})});
It works fine, but when I use withFetch()
in provideHttpClient
, it stops working. Maybe I have to specify something additional during request cloning when using fetch?
@gnibeda can you verify if you can read the cookie succesfully on SSR side? i.e. console.log(ssrCookies)
in the interceptor and see what you see in terminal where the SSR process is running.
@fvoska, yes I can read it. As I said - it works without withFetch()
. Only adding withFetch()
to provideHttpClient
breaks it.
I've tested it with logging, cookies was provided to headers in interceptor but there is no cookies in raw headers on express side when processing API calls. (only when using withFetch()
, if I remove it, it works fine)
I'm submitting a ... (check one with "x")
Current behavior
Currently I see no option to attach cookies to a http request when the application runs on server platform. Using Universal + angular v2.x.x allowed setting 'Cookie' header property manually, in the current version I'm getting the following warning:
Refused to set unsafe header "Cookie"
(node-xhr2 library logs this warning, as it disallows setting cookie header).Expected behavior It should be possible to pass cookies to a http request. It was already asked in the roadmap but no answer or suggestion was provided.
Minimal reproduction of the problem with instructions
What is the motivation / use case for changing the behavior?
Without attaching cookies to a http request, the response on the server is different than the response on the client. It also makes the usage of transfer state dangerous, since the browser will not fire the http request and will use the response received from the server.
Please tell us about your environment:
Angular version: 4.0.0
Browser: [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ]
Language:: TypeScript 2.2.2