How to do make >2 independent / parallel http requests in Angular 2 - node.js

Basic Situation:
I am building a form in my app built with Angular CLI. The form has three select boxes containing long lists of options. These options are populated via http requests to a server. I invoke three methods in ngOnInit to make these http requests (see code below). No matter what sequence I call them in, only the first two requests ever complete.
Eventually the console will show the following error for the third request:
HttpErrorResponse {headers: HttpHeaders, status: 0, statusText: "Unknown Error", url: "http://my-corporate-url:3001/db/depts?deptName=", ok: false, …}
headers: HttpHeaders {normalizedNames: Map(0), lazyUpdate: null, headers: Map(0)}
status: 0
statusText: "Unknown Error"
url: "http://my-corporate-url:3001/db/depts?deptName="
ok: false
name: "HttpErrorResponse"
message: "Http failure response for http://my-corporate-url:3001/db/depts?deptName=: 0 Unknown Error"
error: ProgressEvent {isTrusted: true, lengthComputable: false, loaded: 0, total: 0, type: "error", …}
__proto__: HttpResponseBase
I could "chain" the three http requests to be called sequentially, but I want the http services to be reusable in other forms I build which will have the same select box input elements.
Yes I already Read:
There are a few Stackoverflow questions out there similar to this one (especially this question), but none of them address this specific issue of being able to make only 2 http requests. There is mention of using mergeMap and forkJoin, but these appear to designed for calling an array of identical requests.
My Code:
The component which is making the requets:
import { Component, OnInit } from '#angular/core';
import { NodeHTTPService } from '../node-http.service';
#Component({
...
})
export class NewServiceDeskGroupComponent implements OnInit {
existingGroups;
deptArray;
locArray;
group: string = "";
dept: string = "";
loc: string = "";
constructor(private _nodeHTTPService: NodeHTTPService) { }
ngOnInit() {
this.queryGroupNames();
this.queryBBB();
this.queryDepts();
}
queryGroupNames() {
console.log('begin querying for groups')
this._nodeHTTPService.getSDMGroups().subscribe(
data => {this.existingGroups = data},
err => console.log(err),
() => console.log('done loading groups')
);
}
queryBBB() {
console.log('begin querying for branches')
this._nodeHTTPService.queryBranches(this.loc).subscribe(
data => {this.locArray = data},
err => console.log(err),
() => console.log('done loading branches')
);
}
queryDepts() {
console.log('begin querying for departments')
this._nodeHTTPService.getDepts(this.dept).subscribe(
data => {this.deptArray = data},
err => console.log(err),
() => console.log('done loading departments')
);
}
}
My node-http.service.ts file:
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type' : 'application/json' })
};
#Injectable({
providedIn: 'root'
})
export class NodeHTTPService {
constructor(private http: HttpClient) { }
getSDMGroups() {
let groups = this.http.get('http://my-corporate-url:3001/db/sdm-groups')
return groups;
}
queryBranches(i) {
let bbbs = this.http.get(`http://my-corporate-url:3001/db/bbb-full-service?bbb=${i}`);
return bbbs;
}
getDepts(i) {
if (!i) { i = ''};
let depts = this.http.get(`http://my-corporate-url:3001/db/depts?deptName=${i}`);
return depts;
}
}
What I tried:
I tried commenting out each of the three methods invoked in ngOnInit to see if one of them is erring out. I've also tried shuffling the order of the three method invocations. In every scenario, the browser can successfully execute any 2 of the 3 methods, but the 3rd never completes.
Any pointers? Thank you!

The error you're receiving looks like a bad request. Specifically, I think you're trying to call the departments endpoint without a required dept query param. (This is hard to determine without seeing your server side code)
Your error:
Http failure response for
http://my-corporate-url:3001/db/depts?deptName=: 0 Unknown Error
If you set a correct value for this.dept before invoking your departments endpoint, do you get it to work?
I would also recommend setting a breakpoint in the server code to see what the department's endpoint is receiving from the client. It could be that your server is throwing an exception because you're immediately trying to do something with the query param deptName but it's empty so your api barfs.
Keep in mind, in JavaScript, an empty string ('') is a falsy value.
Since you're calling all your endpoints at once like this:
this.queryGroupNames();
this.queryBBB();
this.queryDepts();
You will need to chain those together if the previous call needs to pass data to the following call. If that isn't the case, and you just want to execute all requests at once, I would recommend using Promise.all
Promise.all([
this._nodeHTTPService.getSDMGroups(),
this._nodeHTTPService.queryBranches(this.loc),
this._nodeHTTPService.getDepts(this.dept)
]);

Related

Hapi Server Side Validation Results on Frontend with Javascript Disabled

I'm investigating using the Hapi framework on Node.js for building a website where the requirement is for it to work fully with javascript disabled in the browser, and also I can't use HTML 5.
When posting form data, I've had a look at Joi for performing validation on the server side which looks good, but instead of just returning an error code, I need to display this nicely in the front end, eg. red borders around the relevant fields with custom error messages for each field and an error summary.
Is there any way of doing this? Or is there perhaps a plugin that would help without using client side javascript?
Looks like I've found a good way of doing it.
Simply return the error data in the view along with all the other data that the page binds to by returning h.view on the post method. Then use whatever templating plugin you are using to render the error message or not depending on the data.
{
method: 'POST',
path: currentPath,
options: {
validate: {
options: { abortEarly: false },
payload: Joi.object({
isAgent: Joi.string().required()
}),
failAction: (request, h, err) => {
const errorList = []
const fieldError = findErrorList(err, [isAgent])[0]
if (fieldError) {
errorList.push({
text: fieldError,
href: `#${field}`
})
}
return h.view(viewTemplate, createModel(errorList, request.payload, null)).takeover()
}
},
handler: async (request, h) => {
return h.redirect(nextPath);
}
},
}

Getting the http post canceled when subscribing from a service

Stuck in this error for a week. Cannot send a simple post request to the heroku server using the Angular HttClient. Defined all the services in the provider section in the main app Module. The Error Handling service is not logging any error after sending the post request(This service works fine that i have tested in another project).
The component is defined in a different module but services are defined and provided in the root app.module.ts
These Modules that components live are imported in the main app module.
But no matter what the post request being canceled!!.
API Params
email: string
password: string
AuthModel
Model using for defining the data params for API
export interface AuthModel {
email: string;
password: string
}
AuthService.ts
This is the service i used to inject into my component.ts file for subscribing. This service also uses another service to handle the error cases HandleError
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
})
};
#Injectable({
providedIn: 'root'
})
export class AuthService {
signUpUrl = 'myurl';
private handleError: HandleError;
constructor(private http: HttpClient, private httpErrorService: HttpErrorService) {
this.handleError = this.httpErrorService.createHandleError('AuthService'); //Problem lies here
}
signUp(authModel: AuthModel) : Observable<AuthModel>
{
return this.http.post<AuthModel>(this.signUpUrl,authModel,httpOptions).pipe( catchError(this.handleError('signup',authModel)) );
}
}
component.ts
Submit function is called when button is clicked after entering the data
Submit(): void {
this.authModel!.email = this.emailHolder.value;
this.authModel!.password = this.passwordHolder.value;
this.authService.signUp(this.authModel).subscribe((res)=> {console.log(res)});
}
The problem was with my handleError function, it get's canceled not matter the response was. So make sure guys to write a proper errorHandling function, else you will get unintended result.

API Controller : Cannot PUT, Cannot DELETE (404 not found)

With Nest.js, and a basic controller :
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '#nestjs/common';
import { Hero } from '../entities/hero.entity';
import { HeroService } from './hero.service';
#Controller('hero')
export class HeroController {
constructor(private readonly heroesService: HeroService) {}
#Get()
async get(#Query() query): Promise<Hero[]> {
return await this.heroesService.find(query);
}
#Get(':id')
async getById(#Param('id') id): Promise<Hero> {
return await this.heroesService.findById(id);
}
#Post()
async add(#Body() hero: Hero): Promise<Hero> {
return await this.heroesService.save(hero);
}
//TODO: doesn't seem to work, never called (request 404)
#Put(':id')
async update(#Param('id') id, #Body() hero): Promise<Hero> {
console.log('hey');
return await this.heroesService.update(id, hero);
}
//TODO: doesn't seem to work, never called (request 404)
#Delete('/delete/:id')
async remove(#Param('id') id): Promise<Hero> {
console.log('hey');
return await this.heroesService.remove(id);
}
}
Following the basic documentation of nest.js, a module with a controller and a service, injecting a typeorm repository for the entity 'Hero'.
Using Postman, both #Get, #Get(':id') and #Post work perfectly, my entity->repository->service->controller connects to my local Postgres DB, and I can get/add/update data from the Hero table with those API endpoints.
However, PUT and DELETE requests respond with :
{
"statusCode": 404,
"error": "Not Found",
"message": "Cannot PUT /hero"
}
X-Powered-By →Express
Content-Type →application/json; charset=utf-8
Content-Length →67
ETag →W/"43-6vi9yb61CRVGqX01+Xyko0QuUAs"
Date →Sun, 02 Dec 2018 11:40:41 GMT
Connection →keep-alive
The request for this is localhost:3000/hero (same endpoint as GET and POST), i've tried either by adding a id:1 in Params or in the Body with x-www-form-urlencoded.
The requests don't ever seem to arrive at the controller (nothing called), i've added a globalinterceptor to Nest.js that just does this :
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
console.log(context.switchToHttp().getRequest());
return call$;
}
But again it only logs GET and POST requests, the others never appear.
What confuses me is that I've pretty much followed the Nest.js doc, made a basic controller and service, entity/repository connected to DB, there doesn't seem to be anything else needed for this to work, and yet PUT and DELETE appear to not exist.
Judging from msg Cannot PUT /hero you are making a /hero request rather than for example /hero/1
The request for this is localhost:3000/hero (same endpoint as GET and POST), i've tried either by adding a id:1 in Params or in the Body with x-www-form-urlencoded.
The PUT request should be done with localhost:3000/hero/<id_here> Think you are confusing query params with path params.
Simmilarly DELETE should be done on localhost:3000/hero/delete/<id_here>

How can I cast input JSON object to model class in Node.JS with TypeScript?

I am writing my Node.js server using TypeScript and express framework.
This is how my controller and route looks like:
export class AuthController {
public async signUpNewUser(request: Request, response: Response) {
...
}
}
How can I receive a model class instead Request type like in ASP.NET ?
Something like:
public async signUpNewUser(input: SignUpModel, response: Response) {
Is this a good idea at all? I am not sure this is a common approach in the Node.JS
I just want to make sure I get the same model each time and write a code related to this model and not on dynamic JSON object.
My suggestion is to convert to strong type model at the beginning of the route, but I am not sure this is a good way.
Does somebody have a solution for such cases?
How can I receive a model class instead Request type like in ASP.NET
This was a perpetual pain to me in my projects (and at work), eventually we decided to build a custom router with its own default error handling and auth-header checks. The trick with this pattern is to keep it lightweight, because this is still express and middleware is where things should go - this wrapper just provides a way for us to cast the express request into a properly shaped type based on the middleware we actually use.
This is pared down example, the idea is that you can specify the shape of the req & res by passing an interface (or an inlined type shape) and have typescript enforce the return shape.
Wrapper class example:
import * as express from 'express';
export type ExpressMethods = "get" | "post" | "put" | "delete" | "patch";
export type JsonRouteInput<RequestBody, RouteParams, QueryParams> = {
body: RequestBody;
params: RouteParams;
query: QueryParams;
};
export type JsonRouteHandler<
RequestBody,
RouteParams,
QueryParams,
ResponseBody
> = (
request: JsonRouteInput<RequestBody, RouteParams, QueryParams>
) => Promise<ResponseBody> | ResponseBody;
export class JsonRouter {
router = express.Router();
private addHandler<M extends ExpressMethods>(
method: M,
...middleware: express.RequestHandler[]
) {
this.router.use(...middleware);
}
get route(): {
[K in ExpressMethods]: <
RequestBody,
ResponseBody,
RouteParams = never,
QueryParams = never
>(
path: string,
handler: JsonRouteHandler<
RequestBody,
RouteParams,
QueryParams,
ResponseBody
>
) => JsonRouter
} {
const addables = {} as any;
(["get", "post", "put", "delete", "patch"] as ExpressMethods[]).forEach(
<RequestBody, ResponseBody, RouteParams = never, QueryParams = never>(
method
) => {
addables[method] = (
path: string,
handler: JsonRouteHandler<
RequestBody,
RouteParams,
QueryParams,
ResponseBody
>
) => {
this.router[method](path, async (req, res) => {
try {
const responseBody: ResponseBody = await handler({
body: req.body,
params: req.params,
query: req.query
});
res.json(responseBody);
} catch (err) {
// do your standard error handling or whatever
res.status(500).end("ow");
}
});
return this;
};
}
);
return addables;
}
}
And then using it
const jsonRouter = new JsonRouter().route.get<{ request: number }, { response: number }>(
"/hello-world",
req => {
return { response: req.body.request + 1 }; // type-checked result
}
);
This can definitely be taken one step further - I have some prototypes that allow us to semi-fluently build the shape of the request/response body. The goal with this strategy long term lets us generate a typescript rest client for the frontend, generate input-validation that matches the type we're using to annotate, and also enforce that the response is the right type - example router using this strategy to build the type dynamically
EDIT: To plug this example into an express server
const app = express();
// custom middleware goes here
app.use('/', jsonRouter.router);
app.listen(8000)
So you seem to have a couple different questions in there. The core question is "how do I cast a JSON object to a specific type", but then you also ask if it's a good idea or not and if it's a common practice.
The answer to your first question is pretty simple, you can cast it in your route (or wherever) like so:
router.get('/whatever', (req, res) => {
const signup: SignupModel = JSON.parse(req.model) as SignupModel;
// Do whatever you want with the signup model
});
Now, your other questions are way more opinion-based. If I'm being honest, I would say "don't use Typescript". :) Joking aside, I don't know how to answer your opinion-based question (nor is it a good fit for this site)

aurelia-fetch-client - a promise was rejected with a non-error: [object Response]

I'm using the aurelia-fetch-client and I get this error when I send a request to my nodejs backend api:
Warning: a promise was rejected with a non-error: [object Response]
at http://localhost:9000/scripts/vendor-bundle.js:39700:20
at Array.reduce (native)
at applyInterceptors (http://localhost:9000/scripts/vendor-bundle.js:39696:33)
at processResponse (http://localhost:9000/scripts/vendor-bundle.js:39688:12)
at http://localhost:9000/scripts/vendor-bundle.js:39603:18
From previous event:
at http://localhost:9000/scripts/vendor-bundle.js:39602:24
From previous event:
at HttpClient.<anonymous> (http://localhost:9000/scripts/vendor-bundle.js:39590:64)
at HttpClient.fetch (http://localhost:9000/scripts/vendor-bundle.js:39574:23)
at AuthService.login (http://localhost:9000/scripts/app-bundle.js:126:30)
at Login.login (http://localhost:9000/scripts/app-bundle.js:190:30)
at CallScope.evaluate (http://localhost:9000/scripts/vendor-bundle.js:24067:21)
at Listener.callSource (http://localhost:9000/scripts/vendor-bundle.js:27508:42)
at http://localhost:9000/scripts/vendor-bundle.js:27532:24
at HTMLDocument.handleDelegatedEvent (http://localhost:9000/scripts/vendor-bundle.js:25721:11)
Everything works fine but this warning is very annoying and I have no idea how to fix it, here's the code that sends the request:
import {HttpClient, json} from 'aurelia-fetch-client';
import baseConfig from 'config';
export class AuthService {
constructor() {
this.http = new HttpClient().configure(config => {
config
.withBaseUrl(baseConfig.baseUrl)
.useStandardConfiguration();
});
this.isAuthenticated = false;
}
login(credentials) {
return this.http.fetch('/login', {
method: 'post',
body: json(credentials)
})
.then(res => {
this.saveToken(res.token)
return Promise.resolve();
});
}
saveToken(token) {
localStorage.setItem('token', token);
this.isAuthenticated = true;
}
}
Any help appreciated
The aurelia-fetch-client standard configuration (applied in your code via .useStandardConfiguration()) rejects on non-success HTTP response status codes. There is a recent (closed) issue for this in the aurelia/fetch-client repo here. The fetch client is rejecting the promise with the response itself, and so the browser is complaining (it wants promises to only be rejected with instances of Error).
I addressed this in my own code by removing .useStandardConfiguration(), as I don't need it in this particular project. You can check out the source code to get more of a picture of what's going on with the configuration.

Resources