Forward fetch response headers to Apollo graphql response - express

I have an apollo-datasource-rest data source setup on an Apollo/Graphql
Expressjs server. I'd like to forward a few headers from the fetch response to the /graphql response.
Here is the flow:
POST /graphql
Graphql makes fetch request using this.get('http://example.com/api')
Response from this fetch contains the header "cache-status"
Response from /graphql
I'd like to include the "cache-status" header from the example.com/api response here in the /graphql response
I see the header in the didReceiveResponse() method of the rest datasource class. I'm not sure this is the right place to access and store it. How do I include the "cache-status" header in the POST /graphql response?

Assuming you're RESTDataSource from apollo-datasource-rest, you can override the didReceiveResponse to intercept the response and return a custom return value.
Here's a code snippet in Typescript, it can be easily converted to Javascript if need be by removing the parameter/return types and access modifiers.
class MyRestDataSource extends RESTDataSource {
public constructor(baseUrl: string) {
super();
this.baseURL = baseUrl;
}
public async getSomething(): Promise<any & { headers?: { [key: string]: string } }> {
// make the get request
return this.get('path/to/something');
}
// intercept response after receiving it
protected async didReceiveResponse(response: Response, _request: Request) {
// get the value that is returned by default, by calling didReceiveResponse from the base class
const defaultReturnValue = await super.didReceiveResponse(response, _request);
// check if it makes sense to replace return value for this type of request
if (_request.url.endsWith('path/to/something')) {
// if yes get the headers from response headers and add it to the returned value
return {
...defaultReturnValue,
headers: { headerValue: response.headers.get('header_name') },
};
}
return defaultReturnValue;
}
}

I think you can utilize formatResponse of ApolloServerLambda()
And respond with proper headers. I have not tried it myself. But looking at the document I feel it should be helpful
formatResponse?: (
response: GraphQLResponse,
requestContext: GraphQLRequestContext<TContext>,
) => GraphQLResponse | null;

Related

Nestjs early return in interceptor sends [ERR_HTTP_HEADERS_SENT]

I am using interceptor on a controller to feed mock data if user is tagged as a demo user in the jwt
It works fine, mock data are fed to client and request doesn't reach my controller, but I still get following error in console :
UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
This is my interceptor :
#Injectable()
export class UserPageStatsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Let controller handle request
if (!isDemo(context)) {
return next.handle();
}
// Feed mock datas
const res = context.switchToHttp().getResponse();
res.status(200).send('Demo data');
}
}
And here is my controller :
#Get('statistics')
#UseInterceptors(UserPageStatsInterceptor)
getUsersGlobalStatistics(
#Headers() headers: CustomHeaders,
#Query() filters: BasicFiltersDTO,
#Req() req,
): Promise<UserStatisticsDTO> {
return 'Real data';
}
I suspect that after my UserPageStatsInterceptor the request continues somewhere in the internal of nestjs framework but I haven't been able to find out where.
I have tried adding a returnor res.end() at the end but it doesn't change anything.
Note that this only happens when making queries on my frontend client and doesn't happen in postman
Thank you in advance guys !!
Instead of calling res.send() inside of your interceptor, why not just return an observable that has the data you want to return?
#Injectable()
export class UserPageStatsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Let controller handle request
if (!isDemo(context)) {
return next.handle();
}
return of('Demo data');
}
}
This will let Nest still handle the response, and not get in the way of actually running your service code.

How to send post request to graphql API in flutter

I'm trying to learn how to use rails combined with graphql to create a rails API by developing a simple app that just retrieves text (in my case, quotes) from a database and shows it on screen. I am using flutter for frontend and rails with graphql as the backend. The backend part was easy to create because I already had some rails knowledge but the frontend part is something I'm new to and I'm trying to figure out how to access a graphql query that I created via flutter to get the data that needs to be displayed.
Below is the flutter code that I currently have (partially adapted from How to build a mobile app from scratch with Flutter and maybe Rails?).
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Quote> fetchQuote() async {
final response =
await http.get('http://10.0.2.2:3000/graphql?query={quote{text}}');
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON.
return Quote.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load quote');
}
}
class Quote {
final String text;
Quote({this.text});
factory Quote.fromJson(Map<String, dynamic> json) {
return Quote(
text: json['text']
);
}
}
void main() => runApp(MyApp(quote: fetchQuote()));
class MyApp extends StatelessWidget {
final Future<Quote> quote;
MyApp({this.quote});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fetch Data Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Fetch Data Example'),
),
body: Center(
child: FutureBuilder<Quote>(
future: quote,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.text);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
),
),
),
);
}
}
Some obvious reasons why this code is wrong that I already figured out myself is that the graphql server expects a post request for the query while my code is sending a get request but that is my question. How do I send a post request for my graphql server in flutter to retrieve the data? The query that I'm trying to access is the one after '?query=' in my flutter code.
This took me a minute to figure out, too, but here is what I did in my practice todo app:
1 - Read this page on graphql post requests over http. There is a section for GET Requests as well as POST.
2 - Make sure your body function argument is correctly json-encoded (see code below).
Tip: Using Postman, you can test the graphql endpoint w/different headers & authorization tokens, and request bodies. It also has a neat feature to generate code from the request. Check out this page for details. It's not 100% accurate, but that's what helped me figure out how to properly format the request body. In the function post, apparently you can't change the content-type if you provide a Map as the body of the request (and the request content types is application/json), so a String worked for my use case.
Sample Code (uses a GqlParser class to properly encode the request body):
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'todo.dart';
import '../creds/creds.dart';
import 'gql_parser.dart';
const parser = GqlParser('bin/graphql');
class TodoApiException implements Exception {
const TodoApiException(this.message);
final String message;
}
class TodoApiClient {
const TodoApiClient();
static final gqlUrl = Uri.parse(Credential.gqlEndpoint);
static final headers = {
"x-hasura-admin-secret": Credential.gqlAdminSecret,
"Content-Type": "application/json",
};
Future<List<Todo>> getTodoList(int userId) async {
final response = await http.post(
gqlUrl,
headers: headers,
body: parser.gqlRequestBody('users_todos', {'userId': userId}),
);
if (response.statusCode != 200) {
throw TodoApiException('Error fetching todos for User ID $userId');
}
final decodedJson = jsonDecode(response.body)['data']['todos'] as List;
var todos = <Todo>[];
decodedJson.forEach((todo) => todos.add(Todo.fromJson(todo)));
return todos;
}
// ... rest of class code ommitted
Per the .post() body argument documentation:
If it's a String, it's encoded using [encoding] and used as the body
of the request. The content-type of the request will default to
"text/plain".
If [body] is a List, it's used as a list of bytes for the body of the
request.
If [body] is a Map, it's encoded as form fields using [encoding]. The
content-type of the request will be set to
"application/x-www-form-urlencoded"; this cannot be overridden.
I simplified the creation of a string to provide as the body of an argument with the following code below, in a GqlParser class. This will allow you to have a folder such as graphql that contains multiple *.graphql queries/mutations. Then you simply use the parser in your other classes that need to make simple graphql endpoint requests, and provide the name of the file (without the extension).
import 'dart:convert';
import 'dart:io';
class GqlParser {
/// provide the path relative to of the folder containing graphql queries, with no trailing or leading "/".
/// For example, if entire project is inside the `my_app` folder, and graphql queries are inside `bin/graphql`,
/// use `bin/graphql` as the argument.
const GqlParser(this.gqlFolderPath);
final String gqlFolderPath;
/// Provided the name of the file w/out extension, will return a string of the file contents
String gqlToString(String fileName) {
final pathToFile =
'${Directory.current.path}/${gqlFolderPath}/${fileName}.graphql';
final gqlFileText = File(pathToFile).readAsLinesSync().join();
return gqlFileText;
}
/// Return a json-encoded string of the request body for a graphql request, given the filename (without extension)
String gqlRequestBody(String gqlFileName, Map<String, dynamic> variables) {
final body = {
"query": this.gqlToString(gqlFileName),
"variables": variables
};
return jsonEncode(body);
}
}

i18n-backend with oauth2 authorization in spartacus

We want to use a backend for i18n in spartacus. Unfortunately this backend needs an oauth2 authentication but spartacus does not send a bearer token when trying to access this webservice endpoint and we get a 401 error. Is there anything we can do?
Right now we try to solve this problem in this way:
What we need to have is implemented in ClientTokenInterceptor, so we adapted this interceptor, changed the if-clause a little bit so it fits to the backend-url for this webservices and provide the interceptor via app.module.ts which works so far. Unfortunately calling this.authService.getClientToken() in our Interceptor returns no token.
constructor(
private authService: AuthService,
private occEndpoints: OccEndpointsService
) {}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return this.getClientToken(request).pipe(
take(1),
switchMap((token: ClientToken) => {
if (
token &&
request.url.includes("i18n")
) {
request = request.clone({
setHeaders: {
Authorization: `${token.token_type} ${token.access_token}`,
},
});
}
return next.handle(request);
})
);
}
private getClientToken(request: HttpRequest<any>): Observable<ClientToken> {
if (
InterceptorUtil.getInterceptorParam(USE_CLIENT_TOKEN, request.headers)
) {
return this.authService.getClientToken();
}
return of(null);
}
What do we miss?
Actually there are couple of things not needed in your solution.
I pasted below what I did instead and tested that it is working correctly (and you can see authorization data in the translation files requests).
First issue:
InterceptorUtil.getInterceptorParam(USE_CLIENT_TOKEN, request.headers) you don't need to check that. If you always need the auth data for translation requests just use return this.authService.getClientToken();
Second issue:
In intercept method you didn't cover cases for any other request than translation. Because of that the request for the client token would hang here, because it would wait for token and so on. If you add option for any other case than i18n it starts working as you intend.
Working solution:
#Injectable({ providedIn: 'root' })
export class TranslationsInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
if (request?.url?.includes('i18n')) {
return this.getClientToken().pipe(
take(1),
switchMap((token: ClientToken) => {
if (token) {
request = request.clone({
setHeaders: {
Authorization: `${token.token_type} ${token.access_token}`,
},
});
}
return next.handle(request);
})
);
} else {
return next.handle(request);
}
}
private getClientToken(): Observable<ClientToken> {
return this.authService.getClientToken();
}
}

Resend unathorized requests after changing token in Angualr8

I'm new to angular 8.
I have an interceptor :
export class HttpRequestInterceptor implements HttpInterceptor {
private apiAddress = 'http://localhost:1080';
private refreshTokenIsInProgress = false;
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
let cloneReq;
//...
// some codes like adding access token to header
//...
cloneReq = request.clone();
return next.handle(cloneReq)
.pipe(
catchError((error: any) => {
if(error.status==401)//means token expired
{
//Here i need help
//Get NEW Token And Replace With previous And Resend Current Request
}
return of(error);
})
)
;
}
}
export const httpInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true },
];
my requests are like:
return this.http.get(url).subscribe();
As I mentioned,if request returns 401(unauthorized) I need to get new token (JWT) from server , replace it with previous one, and then resend CURRENT request;
there is no problem on getting new token!
the problem is replacing new token and resend request;
i searched and googled form many hours but ...
i thought i can reach it by using retry() and retryWhen() , but they can't change request parameters.
thank you all buddy
Haven't tested the code, but you can try wrap the request into higher order function and get access to request object, also allow function to be called recursively until condition is met.
const repeatRequest=(cloneReq)=>next.handle(cloneReq)
.pipe(
catchError((error: any) => {
if(error.status==401)//means token expired
req=........ // modify your request here
return repeatRequest(req)
return of(error);
})
)
cloneReq = request.clone();
return repeatRequest(cloneReq)

Setting Headers (token) of multiple objects once user is authenticated in Angular2

Hi I've created a generic service from which I can create objects with generic http requests.
The challenge I'm facing now is to pass to each created object a token into their headers if the user is authenticated (i will get a token as response, which is stored in the localStorage)
So basically I can create these custom http objects anywhere (component-wise by injection) on any level. All of them don't have the Authentication Header set yet. Once User is authenticated, all these object to have their Authentication Header set.
Here's the plunker
export class App {
myHttpObject1;
constructor(private myAuth:MyAuth, private myDatabase:MyDatabase) {
this.name = 'Angular2 (Release Candidate!)'
this.myHttpObject1 = this.myDatabase.httpSchema('users')
this.myHttpObject1.log()
// this.myHttpObject1.someOtherMethodes()...
}
login(){
this.myAuth.login()
}
showHeaders(){
this.myHttpObject1.log()
}
}
Below is the Service and one to mock a login.
#Injectable()
export class MyDatabase{
private base_url:string;
private headers :Headers;
constructor(){
this.base_url = 'https://jsonplaceholder.typicode.com/';
this.headers = new Headers();
this.headers.append('Content-Type', 'application/json');
this.headers.append('Authorization','');
}
public httpSchema(path:string){
return new MyHttpObject(path, this.headers || new Headers())
}
}
class MyHttpObject{
constructor(public url:string, public headers:Headers){
}
log(){
console.log(this.url)
console.log(this.headers)
}
post(){
console.log('here could be a http post')
}
}
#Injectable()
export class MyAuth{
login(){
setTimeout(()=>{
console.log('logged In');
localStorage.setItem('token':'mytoken');
},2000)
}
}
Look at the Http Injector, it intercepts the call and adds whatever you need to the http object.