Sporadic timeouts from directline endpoint - networking

We have deployed a simple Echobot using the Azure template when creating a Web App Bot.
It is currently live at this url: http://18.194.88.194/echobot/
We often experience the following timeout error (about 40% of the time)
WebSocket connection to
'wss://directline.botframework.com/v3/directline/conversations/EH9EbbBIasz8o90sZSeAwT-9/stream?watermark=-&t=ew0KICAiYWxnIj...(snip)
failed: Error in connection establishment:
net::ERR_CONNECTION_TIMED_OUT
Here is our client code:
botclient.js
(async function() {
let {token, conversationId} = sessionStorage;
const delay = 1800000;
//const delay = 20000;
const currentTime = new Date();
const currentTimeUnix = currentTime.getTime();
if (
sessionStorage['startTime'] &&
currentTimeUnix - sessionStorage['startTime'] > delay
) {
sessionStorage.removeItem('token');
token = sessionStorage['token'];
}
const payload = JSON.stringify({botname: 'echobot'});
if (!token) {
const res = await fetch(
'https://ox38xh0fx5.execute-api.eu-central-1.amazonaws.com/dev/directline/token',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: payload,
},
);
const {token: directLineToken} = await res.json();
sessionStorage['token'] = directLineToken;
token = directLineToken;
console.log('token', token);
const startTime = new Date();
const startTimeUnix = startTime.getTime();
sessionStorage['startTime'] = startTimeUnix;
}
const data = {
// from: user,
name: 'requestWelcomeDialog',
type: 'event',
value: {
url: window.location.href,
},
};
var botConnection = new window.WebChat.createDirectLine({token});
window.WebChat.renderWebChat(
{
directLine: botConnection,
userID: 'YOUR_USER_ID',
username: 'Web Chat User',
locale: 'en-US',
},
document.getElementById('webchat'),
);
})().catch(err => console.log(err));
index.html
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.botframework.com/botframework-webchat/latest/webchat.js"></script>
<style>
html,
body {
height: 100%;
}
body {
margin: 0;
}
#webchat {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="webchat" role="main"></div>
<script src="botclient.js">
</script>
</body>
</html>
The token is being retrieved from an AWS lambda function with the following code:
import json
import boto3
from urllib.parse import urlencode
from urllib.request import Request, urlopen
BOT_SECRET = 'defaultSecret'
URL = 'https://directline.botframework.com/v3/directline/tokens/generate'
POST_FIELDS = {'foo': 'bar'}
def set_headers(req, secret):
req.add_header('Content-Type', 'application/json')
req.add_header('Authorization', f'Bearer {secret}')
return req
def lambda_handler(event, context):
print(event)
s3 = boto3.client('s3')
if event.get('botname', None) == 'echobot':
response = s3.get_object(Bucket='directline', Key='echobotSecret')
else:
response = s3.get_object(Bucket='directline', Key=BOT_SECRET)
secret = response['Body'].read().decode('utf-8').rstrip()
request = Request(URL, urlencode(POST_FIELDS).encode())
request = set_headers(request, secret)
jsonresponse = urlopen(request).read()
print(jsonresponse)
return jsonresponse
We suspect that there is an issue with the local network configuration, and packets are being dropped. Are there any pointers for how to handle this?

I looked into our backend logs for the conversation ID you included and it looks like the token needs to be refreshed. See the Authentication docs for refreshing tokens.
It looks like your code attempts to account for token expiration, but:
1800000 is the default expiration. It might be best to shorten your delay a bit to account for request travel time and other factors to ensure your tokens don't expire.
You appear to only get the token once, when the chat window is opened. It may expire mid-conversation
That being said, this may not be a token issue. Can you try disabling websockets with:
[...]
window.WebChat.renderWebChat(
{
directLine: botConnection,
userID: 'YOUR_USER_ID',
username: 'Web Chat User',
locale: 'en-US',
webSocket: false // ADD THIS !!!!
},
document.getElementById('webchat'),
[...]
If this is a local packet loss issue, I'm afraid there's not much support I can provide as it's no longer a botframework problem and is likely very specific to your local network. If you can provide additional Conversation IDs, I might be able to dig into this further. But over the last 2 days, your bot appears pretty normal on our side.

Related

NextAuth - Handle refresh token rotation (Reddit) with database

Reddit's access token has an expiration of 1 hour, but I want users that log in to my app to be able to post comments on Reddit for example. This means I need to refresh their access token once it has expired. Since I'm using a database (PlanetScale + Prisma) and not a JWT strategy, the documentation found here https://next-auth.js.org/tutorials/refresh-token-rotation is not useful to me (jwt callback is never called).
As far as I'm understanding it, it means it's not really possible to check the expiration in the session callback and refresh the token here without accessing the database each time?
What can I do if I want to refresh the access token in my database? Should I use a JWT strategy instead, even though I'm using a database?
To do refresh token rotation when using a database strategy you can do something like this:
async function refreshAccessToken(session: Session) {
if (!session.user?.id) {
return;
}
const {
id,
refresh_token: refreshToken,
expires_at: expiresAt,
} = (await prisma.account.findFirst({
where: { userId: session.user.id, provider: "reddit" },
})) ?? {};
if (!id || !refreshToken) {
return;
}
// If expired refresh it
if (expiresAt && Date.now() / 1000 > expiresAt) {
const authorizationString = Buffer.from(
`${process.env?.["REDDIT_CLIENT_ID"]}:${process.env?.["REDDIT_CLIENT_SECRET"]}`,
).toString("base64");
const headers = {
Authorization: `Basic ${authorizationString}`,
"Content-Type": "application/x-www-form-urlencoded",
};
const urlSearchParams = new URLSearchParams();
urlSearchParams.append("grant_type", "refresh_token");
urlSearchParams.append("refresh_token", refreshToken);
urlSearchParams.append("redirect_uri", `${process.env?.["NEXTAUTH_URL"]}/api/auth/callback/reddit`);
const { data } = await axios.post<RedditResponse>("https://www.reddit.com/api/v1/access_token", urlSearchParams, {
headers,
});
await prisma.account.update({
where: { id },
data: {
access_token: data.access_token,
expires_at: Math.floor(Date.now() / 1000) + data.expires_in,
refresh_token: data.refresh_token,
token_type: data.token_type,
scope: data.scope,
},
});
}
}
You can use this anywhere I guess. I don't know if it makes sense to use this in the session callback or not since it's probably a performance hit, so maybe just call it each time you actually need the access token for something? I'm not knowledgable about this to know what the best practice is in this regard...
After many hours of tinkering i just found out how to get the refresh token into the database!
following the first part of the next auth token refresh tutorial, add the authorization param to the provider options
const GOOGLE_AUTHORIZATION_URL =
"https://accounts.google.com/o/oauth2/v2/auth?" +
new URLSearchParams({
prompt: "consent",
access_type: "offline",
response_type: "code",
});
and
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
authorization: GOOGLE_AUTHORIZATION_URL,
}),
This will send me well on my way to figuring out the rest of the process... hope it works for you too!

How do I make an M-Pesa Callback URL using Firebase Cloud Firestore?

I'm trying to make an app that can send payments to PayBill numbers with Safaricom's "Lipa Na M-Pesa" (a Kenyan thing). The call is a POST request to URL:
https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest
with header:
{
'Host': 'sandbox.safaricom.co.ke',
'Authorization': 'Bearer ${await mpesaAccessToken}',
'Content-Type': 'application/json',
}
and body:
{
"BusinessShortCode": "$businessShortCode",
"Password": "${generateLnmPassword(timeStamp)}",
"Timestamp": "$timeStamp",
"TransactionType": "CustomerPayBillOnline",
"Amount": "10",
"PartyA": "$userPhoneNumber",
"PartyB": "$businessShortCode",
"PhoneNumber": "$userPhoneNumber",
"CallBackURL": "?????????????????????????????",
"AccountReference": "account",
"TransactionDesc": "test",
}
I've received an access token, generated a password and made the call successfully, except for that CallBackURL thing... The M-Pesa docs describe their callback like this:
CallBackURL
This is the endpoint where you want the results of the transaction delivered. Same rules for Register URL API callbacks apply.
all API callbacks from transactional requests are POST requests, do not expect GET requests for callbacks. Also, the data is not formatted into application/x-www-form-urlencoded format, it is application/json, so do not expect the data in the usual POST fields/variables of your language, read the results directly from the incoming input stream.
(More info here, but you may need to be logged in: https://developer.safaricom.co.ke/get-started see "Lipa na M-Pesa")
My app is hosted on Firebase Cloud Firestore. Is there any way I can create a callback URL with them that will receive their callback as a document in a Firestore collection?...
Or would this be impossible, given that they would need authorization tokens and stuff to do so... and I can't influence what headers and body M-Pesa will send?
(PS Btw, I code in Flutter/Dart so plz don't answer in Javascript or anything! I'll be clueless... :p Flutter/Dart or just plain text will be fine. Thanks!)
Is there any way I can create a callback URL with them that will
receive their callback as a document in a Firestore collection?...
The most common way to do that in the Firebase ecosystem is to write an HTTPS Cloud Function that will be called by the Safaricom service.
Within the Cloud Function you will be able to update the Firestore document, based on the content of the POST request.
Something like:
exports.safaricom = functions.https.onRequest((req, res) => {
// Get the header and body through the req variable
// See https://firebase.google.com/docs/functions/http-events#read_values_from_the_request
return admin.firestore().collection('...').doc('...').update({ foo: bar })
.then(() => {
res.status(200).send("OK");
})
.catch(error => {
// ...
// See https://www.youtube.com/watch?v=7IkUgCLr5oA&t=1s&list=PLl-K7zZEsYLkPZHe41m4jfAxUi0JjLgSM&index=3
})
});
I did note that you ask us to not "answer in Javascript or anything" but in Flutter/Dart, but I don't think you will able to implement that in Flutter: you need to implement this webhook in an environment that you fully control and that exposes an API endpoint, like your own server or a Cloud Function.
Cloud Functions may seem complex at first sight, but implementing an HTTPS Cloud Functions is not that complicated. I suggest you read the Get Started documentation and watch the three videos about "JavaScript Promises" from the Firebase video series, and if you encounter any problem, ask a new question on SO.
Cloud functions are not Dart-based.
See below solution;
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const parse = require("./parse");
admin.initializeApp();
exports.lmno_callback_url = functions.https.onRequest(async (req, res) => {
const callbackData = req.body.Body.stkCallback;
const parsedData = parse(callbackData);
let lmnoResponse = admin.firestore().collection('lmno_responses').doc('/' + parsedData.checkoutRequestID + '/');
let transaction = admin.firestore().collection('transactions').doc('/' + parsedData.checkoutRequestID + '/');
let wallets = admin.firestore().collection('wallets');
if ((await lmnoResponse.get()).exists) {
await lmnoResponse.update(parsedData);
} else {
await lmnoResponse.set(parsedData);
}
if ((await transaction.get()).exists) {
await transaction.update({
'amount': parsedData.amount,
'confirmed': true
});
} else {
await transaction.set({
'moneyType': 'money',
'type': 'deposit',
'amount': parsedData.amount,
'confirmed': true
});
}
let walletId = await transaction.get().then(value => value.data().toUserId);
let wallet = wallets.doc('/' + walletId + '/');
if ((await wallet.get()).exists) {
let balance = await wallet.get().then(value => value.data().moneyBalance);
await wallet.update({
'moneyBalance': parsedData.amount + balance
})
} else {
await wallet.set({
'moneyBalance': parsedData.amount
})
}
res.send("Completed");
});
Parse function.
const moment = require("moment");
function parse(responseData) {
const parsedData = {};
parsedData.merchantRequestID = responseData.MerchantRequestID;
parsedData.checkoutRequestID = responseData.CheckoutRequestID;
parsedData.resultDesc = responseData.ResultDesc;
parsedData.resultCode = responseData.ResultCode;
if (parsedData.resultCode === 0) {
responseData.CallbackMetadata.Item.forEach(element => {
switch (element.Name) {
case "Amount":
parsedData.amount = element.Value;
break;
case "MpesaReceiptNumber":
parsedData.mpesaReceiptNumber = element.Value;
break;
case "TransactionDate":
parsedData.transactionDate = moment(
element.Value,
"YYYYMMDDhhmmss"
).unix();
break;
case "PhoneNumber":
parsedData.phoneNumber = element.Value;
break;
}
});
}
return parsedData;
}
module.exports = parse;

Axios post request to Firebase Auth REST API produces 400 error

I have an instance of Axios:
import axios from 'axios';
const instance = axios.create({
baseURL: 'https://identitytoolkit.googleapis.com/v1'
});
export default instance;
Then I import it in my signup.vue file:
<script>
import axios from '../../axios-auth';
...
</script>
In that Vue file I have a signup form, which runs the following method once I hit the Submit button:
onSubmit() {
const formData = {
email: this.email,
age: this.age,
password: this.password,
confirmPassword: this.confirmPassword,
country: this.country,
hobbies: this.hobbyInputs.map(hobby => hobby.value),
terms: this.terms
};
console.log(formData);
axios.post('/accounts:signUp?key=my_key_goes_here', {
email: formData.email,
password: formData.password,
returnSecureToken: true
})
.then(res => {
console.info(res);
})
.catch(error => {
console.error(error);
});
}
I'm getting a 403 error - forbidden 400 error - bad request.
I tried to change headers:
instance.defaults.headers.post["Access-Control-Allow-Origin"] = "localhost";
instance.defaults.headers.common["Content-Type"] = "application/json";
But that didn't help.
I'm working from localhost and I saw that localhost is allowed by default. I tried also to add 127.0.0.1 to the list, but that also didn't help.
What am I missing? How can I make this request work?
If you get a 400 error it is maybe because you get an error from the API itself:
Common error codes
EMAIL_EXISTS: The email address is already in use by another account.
OPERATION_NOT_ALLOWED: Password sign-in is disabled for this project.
TOO_MANY_ATTEMPTS_TRY_LATER: We have blocked all requests from this device due to unusual activity. Try again later.
As a matter of fact, those errors return an HTTP Status Code of 400.
You can see the exact response message (e.g. EMAIL_EXISTS) by doing the following with axios:
axios.post('/accounts:signUp?key=my_key_goes_here', {
email: formData.email,
password: formData.password,
returnSecureToken: true
})
.then(res => {
console.info(res);
})
.catch(error => {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log(error.response.data);
} else if (error.request) {
console.log(error.request);
} else {
console.log("Error", error.message);
}
});
See https://github.com/axios/axios#handling-errors
I agree with you as i have tried many approaches but was not getting the result. Hence i have tried to change the code.
You need to make two changes in your code.
1] You need to comment the instance.defaults.headers.post["Access-Control-Allow-Origin"] = "localhost"; because you are providing the authentication globally. As, firebase provides the feature of authentication and you are connecting the web app with REST API.
2] You need to add { headers: {'Content-Type': 'application/json' } in the axios.post() method to prevent it from CORS Error.
Following this approach i hope you can get the respective output.
Happy Coding!
Directly call
https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[yourkey]
No need to keep it in a separate file
Anyone who comes to the thread in future. I faced this issue and lost in debugging and worked with fetch. It was tiresome and took me a day but i made axios work. Here is the code.
const data = JSON.stringify({
idToken: authContext.token,
password: enteredNewPassword,
returnSecureToken: false,
});
// Send the valid password to the endpoint to change password
axios
.post(
"https://identitytoolkit.googleapis.com/v1/accounts:update?key=[Your Key]",
data,
{
headers: {
"Content-Type": "application/json",
},
}
)
.then((response) => {
console.log(response.data);
})
.catch((err) => {
console.log(err.message);
});
Remember to Stringify the data you want to send. Stringify it outside of the http request and then pass that variable. Don't know why but this helps!
Lastly remember to add the header when sending the request to firebase. Make sure axios.post is on the same line. My formatter gave a line break which was also cause of error.
Hope it helps :)

fetching mp3 file from MeteorJS and trying to convert it into a Blob so that I can play it

am playing around with downloading and serving mp3 files in Meteor.
I am trying to download an MP3 file (https://www.sample-videos.com/audio/mp3/crowd-cheering.mp3) on my MeteorJS Server side (to circumvent CORS issues) and then pass it back to the client to play it in a AUDIO tag.
In Meteor you use the Meteor.call function to call a server method. There is not much to configure, it's just a method call and a callback.
When I run the method I receive this:
content:
"ID3���#K `�)�<H� e0�)������1������J}��e����2L����������fȹ\�CO��ȹ'�����}$A�Lݓ����3D/����fijw��+�LF�$?��`R�l�YA:A��#�0��pq����4�.W"�P���2.Iƭ5��_I�d7d����L��p0��0A��cA�xc��ٲR�BL8䝠4���T��..etc..", data:null,
headers: {
accept-ranges:"bytes",
connection:"close",
content-length:"443926",
content-type:"audio/mpeg",
date:"Mon, 20 Aug 2018 13:36:11 GMT",
last-modified:"Fri, 17 Jun 2016 18:16:53 GMT",
server:"Apache",
statusCode:200
which is the working Mp3 file (the content-length is exactly the same as the file I write to disk on the MeteorJS Server side, and it is playable).
However, my following code doesn't let me convert the response into a BLOB:
```
MeteorObservable.call( 'episode.download', episode.url.url ).subscribe( ( result: any )=> {
console.log( 'response', result);
let URL = window.URL;
let blob = new Blob([ result.content ], {type: 'audio/mpeg'} );
console.log('blob', blob);
let audioUrl = URL.createObjectURL(blob);
let audioElement:any = document.getElementsByTagName('audio')[0];
audioElement.setAttribute("src", audioUrl);
audioElement.play();
})
When I run the code, the Blob has the wrong size and is not playable
Blob(769806) {size: 769806, type: "audio/mpeg"}
size:769806
type:"audio/mpeg"
__proto__:Blob
Uncaught (in promise) DOMException: Failed to load because no supported source was found.
On the backend I just run a return HTTP.get( url ); in the method which is using import { HTTP } from 'meteor/http'.
I have been trying to use btoa or atob but that doesn't work and as far as I know it is already a base64 encoded file, right?
I am not sure why the Blob constructor creates a larger file then the source returned from the backend. And I am not sure why it is not playing.
Can anyone point me to the right direction?
Finally found a solution that uses request instead of Meteor's HTTP:
First you need to install request and request-promise-native in order to make it easy to return your result to clients.
$ meteor npm install --save request request-promise-native
Now you just return the promise of the request in a Meteor method:
server/request.js
import { Meteor } from 'meteor/meteor'
import request from 'request-promise-native'
Meteor.methods({
getAudio (url) {
return request.get({url, encoding: null})
}
})
Notice the encoding: null flag, which causes the result to be binary. I found this in a comment of an answer related to downloading binary data via node. This causes not to use string but binary representation of the data (I don't know how but maybe it is a fallback that uses Node Buffer).
Now it gets interesting. On your client you wont receive a complex result anymore but either an Error or a Uint8Array which makes sense because Meteor uses EJSON to send data over the wires with DDP and the representation of binary data is a Uint8Array as described in the documentation.
Because you can just pass in a Uint8Array into a Blob you can now easily create the blob like so:
const blob = new Blob([utf8Array], {type: 'audio/mpeg'})
Summarizing all this into a small template if could look like this:
client/fetch.html
<template name="fetch">
<button id="fetchbutton">Fetch Mp3</button>
{{#if source}}
<audio id="player" src={{source}} preload="none" content="audio/mpeg" controls></audio>
{{/if}}
</template>
client/fetch.js
import { Template } from 'meteor/templating'
import { ReactiveVar } from 'meteor/reactive-var'
import './fetch.html'
Template.fetch.onCreated(function helloOnCreated () {
// counter starts at 0
this.source = new ReactiveVar(null)
})
Template.fetch.helpers({
source () {
return Template.instance().source.get()
},
})
Template.fetch.events({
'click #fetchbutton' (event, instance) {
Meteor.call('getAudio', 'https://www.sample-videos.com/audio/mp3/crowd-cheering.mp3', (err, uint8Array) => {
const blob = new Blob([uint8Array], {type: 'audio/mpeg'})
instance.source.set(window.URL.createObjectURL(blob))
})
},
})
Alternative solution is adding a REST endpoint *using Express) to your Meteor backend.
Instead of HTTP we use request and request-progress to send the data chunked in case of large files.
On the frontend I catch the chunks using https://angular.io/guide/http#listening-to-progress-events to show a loader and deal with the response.
I could listen to the download via
this.http.get( 'the URL to a mp3', { responseType: 'arraybuffer'} ).subscribe( ( res:any ) => {
var blob = new Blob( [res], { type: 'audio/mpeg' });
var url= window.URL.createObjectURL(blob);
window.open(url);
} );
The above example doesn't show progress by the way, you need to implement the progress-events as explained in the angular article. Happy to update the example to my final code when finished.
The Express setup on the Meteor Server:
/*
Source:http://www.mhurwi.com/meteor-with-express/
## api.class.ts
*/
import { WebApp } from 'meteor/webapp';
const express = require('express');
const trackRoute = express.Router();
const request = require('request');
const progress = require('request-progress');
export function api() {
const app = express();
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.use('/episodes', trackRoute);
trackRoute.get('/:url', (req, res) => {
res.set('content-type', 'audio/mp3');
res.set('accept-ranges', 'bytes');
// The options argument is optional so you can omit it
progress(request(req.params.url ), {
// throttle: 2000, // Throttle the progress event to 2000ms, defaults to 1000ms
// delay: 1000, // Only start to emit after 1000ms delay, defaults to 0ms
// lengthHeader: 'x-transfer-length' // Length header to use, defaults to content-length
})
.on('progress', function (state) {
// The state is an object that looks like this:
// {
// percent: 0.5, // Overall percent (between 0 to 1)
// speed: 554732, // The download speed in bytes/sec
// size: {
// total: 90044871, // The total payload size in bytes
// transferred: 27610959 // The transferred payload size in bytes
// },
// time: {
// elapsed: 36.235, // The total elapsed seconds since the start (3 decimals)
// remaining: 81.403 // The remaining seconds to finish (3 decimals)
// }
// }
console.log('progress', state);
})
.on('error', function (err) {
// Do something with err
})
.on('end', function () {
console.log('DONE');
// Do something after request finishes
})
.pipe(res);
});
WebApp.connectHandlers.use(app);
}
and then add this to your meteor startup:
import { Meteor } from 'meteor/meteor';
import { api } from './imports/lib/api.class';
Meteor.startup( () => {
api();
});

Angular2 ( 2.2.1 ) Http post request progress bar [duplicate]

Is there currently a way within Angular 2 to retrieve the progress (i.e. percentage done) of an ajax call, using the angular2/http module?
I use the following code to make my HTTP calls:
let body = JSON.stringify(params);
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
this.http.post(url, body, options)
.timeout(10000, new Error('Timeout exceeded during login'))
.toPromise()
.then((res) => {
...
}).catch((err) => {
...
});
The goal is to write a synchronisation system. The post will return a lot of data, and I want to give the user an indication on how long the syncing will take.
Currently (from v. 4.3.0, when using new HttpClient from #ngular/common/http) Angular provides listening to progress out of the box. You just need to create HTTPRequest object as below:
import { HttpRequest } from '#angular/common/http';
...
const req = new HttpRequest('POST', '/upload/file', file, {
reportProgress: true,
});
And when you subscribe to to request you will get subscription called on every progress event:
http.request(req).subscribe(event => {
// Via this API, you get access to the raw event stream.
// Look for upload progress events.
if (event.type === HttpEventType.UploadProgress) {
// This is an upload progress event. Compute and show the % done:
const percentDone = Math.round(100 * event.loaded / event.total);
console.log(`File is ${percentDone}% uploaded.`);
} else if (event instanceof HttpResponse) {
console.log('File is completely uploaded!');
}
});
More info here.
You could leverage the onprogress event provided by XHR (see this plunkr: http://plnkr.co/edit/8MDO2GsCGiOJd2y2XbQk?p=preview).
This allows to get hints about the progress of the download. This isn't supported out of the box by Angular2 but you can plug it by extended the BrowserXhr class:
#Injectable()
export class CustomBrowserXhr extends BrowserXhr {
constructor(private service:ProgressService) {}
build(): any {
let xhr = super.build();
xhr.onprogress = (event) => {
service.progressEventObservable.next(event);
};
return <any>(xhr);
}
}
and override the BrowserXhr provider with the extended:
bootstrap(AppComponent, [
HTTP_PROVIDERS,
provide(BrowserXhr, { useClass: CustomBrowserXhr })
]);
See this question for more details:
Angular2 / RxJS - updating variable after getting data from Http observable
When you make http cals in angular2, it returns an Observable of type Response, this response is created inside class called XHRConnection where all the magic happens.
The XHRConnection builds the response by listening to XMLHttpRequest's load event, this means it will return one response at the end of the request.
Now to be able to alter this behavior we need to make our connection class listen to the progress event.
So we need to create custom Connection class, to handle the response as we see fit.
I did it this way,
Take note that my php API returns multi response in a single request and this responses are plain strings.
my_backend.ts
import {Injectable} from "angular2/core";
import {Observable} from "rxjs/Observable";
import {Observer} from "rxjs/Observer";
import {Connection,ConnectionBackend} from "angular2/src/http/interfaces";
import {ReadyState, RequestMethod, ResponseType} from "angular2/src/http/enums";
import {ResponseOptions} from "angular2/src/http/base_response_options";
import {Request} from "angular2/src/http/static_request";
import {Response} from "angular2/src/http/static_response";
import {BrowserXhr} from "angular2/src/http/backends/browser_xhr";
import {Headers} from 'angular2/src/http/headers';
import {isPresent} from 'angular2/src/facade/lang';
import {getResponseURL, isSuccess} from "angular2/src/http/http_utils"
export class MyConnection implements Connection {
readyState: ReadyState;
request: Request;
response: Observable<Response>;
constructor(req: Request, browserXHR: BrowserXhr, baseResponseOptions?: ResponseOptions) {
this.request = req;
this.response = new Observable<Response>((responseObserver: Observer<Response>) => {
let _xhr: XMLHttpRequest = browserXHR.build();
_xhr.open(RequestMethod[req.method].toUpperCase(), req.url);
// save the responses in array
var buffer :string[] = [];
// load event handler
let onLoad = () => {
let body = isPresent(_xhr.response) ? _xhr.response : _xhr.responseText;
//_xhr.respons 1 = "Loading data!"
//_xhr.respons 2 = "Loading data!Ready To Receive Orders."
// we need to fix this proble
// check if the current response text contains the previous then subtract
// NOTE: I think there is better approach to solve this problem.
buffer.push(body);
if(buffer.length>1){
body = buffer[buffer.length-1].replace(buffer[buffer.length-2],'');
}
let headers = Headers.fromResponseHeaderString(_xhr.getAllResponseHeaders());
let url = getResponseURL(_xhr);
let status: number = _xhr.status === 1223 ? 204 : _xhr.status;
let state:number = _xhr.readyState;
if (status === 0) {
status = body ? 200 : 0;
}
var responseOptions = new ResponseOptions({ body, status, headers, url });
if (isPresent(baseResponseOptions)) {
responseOptions = baseResponseOptions.merge(responseOptions);
}
let response = new Response(responseOptions);
//check for the state if not 4 then don't complete the observer
if(state !== 4){
//this will return stream of responses
responseObserver.next(response);
return;
}
else{
responseObserver.complete();
return;
}
responseObserver.error(response);
};
// error event handler
let onError = (err: any) => {
var responseOptions = new ResponseOptions({ body: err, type: ResponseType.Error });
if (isPresent(baseResponseOptions)) {
responseOptions = baseResponseOptions.merge(responseOptions);
}
responseObserver.error(new Response(responseOptions));
};
if (isPresent(req.headers)) {
req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(',')));
}
_xhr.addEventListener('progress', onLoad);
_xhr.addEventListener('load', onLoad);
_xhr.addEventListener('error', onError);
_xhr.send(this.request.text());
return () => {
_xhr.removeEventListener('progress', onLoad);
_xhr.removeEventListener('load', onLoad);
_xhr.removeEventListener('error', onError);
_xhr.abort();
};
});
}
}
#Injectable()
export class MyBackend implements ConnectionBackend {
constructor(private _browserXHR: BrowserXhr, private _baseResponseOptions: ResponseOptions) {}
createConnection(request: Request):MyConnection {
return new MyConnection(request, this._browserXHR, this._baseResponseOptions);
}
}
And in the app.component.ts
import {Component, provide} from 'angular2/core';
import {HTTP_PROVIDERS,XHRBackend} from 'angular2/http';
import {MyBackend} from './my_backend';
#Component({
selector: 'my-app',
providers: [
HTTP_PROVIDERS,
MyBackend,
provide(XHRBackend, {useExisting:MyBackend})
]
.
.
.
Now calling http.get will return a steam of responses.
#Bartek Chichoki's answer is correct but it was not working for my case.
Adding observe: 'events' did the trick for me
const req = new HttpRequest('POST', '/upload/file', file, {
reportProgress: true,
observe: 'events'
});
Hope it helps
I strongly recomend using this
https://www.npmjs.com/package/angular-progress-http
otherwise messing around with xhr will make you miss sessions cookies and other stuffs
besides it'll be more portable and way easier to implement

Resources