What should I return in an https callable function if it doesn't expect to return nothing - javascript

I have implemented a HTTPs (onCall) function that throws some errors to the client or return true if the work is successfully completed. The problem is that I don't see why to return true (because when I throw the errors I don't return false).
As HTTP protocol requires to return a response to the client to finish a request, what should I return to the client? I am thinking to remove the errors I throw and return a classic HTTP reponse (status code, body, ...).
Any ideas? Here is what I am doing:
exports.function = functions
.region("us-central1")
.runWith({ memory: "2GB", timeoutSeconds: 120 })
.https.onCall(async (data, context) => {
// Lazy initialization of the Admin SDK
if (!is_function_initialized) {
// ... stuff
is_uploadImage_initialized = true;
}
// ... asynchronous stuff
// When all promises has been resolved...
// If work completed successfully
return true;
/*
Is it correct instead ???
return {code: "200 OK", date: date, body: message };
*/
// Else, if errors
throw new Error("Please, try again later.");
/*
Is it correct instead ???
return {code: "418 I'm a teapot", date: date, body: message };
*/
}

As explained in the doc:
To use HTTPS Callable Functions you must use the client SDK for your
platform together with the functions.https backend API (or implement
the protocol)
which means that you must follow the protocol in any case, since the Client SDKs do implement the protocol.
So let's look at what says the protocol about the response to be sent to the client (i.e. the caller or consumer):
The protocol specifies the format of the Response Body as follows:
The response from a client endpoint is always a JSON object. At a
minimum it contains either data or error, along with any optional
fields. If the response is not a JSON object, or does not contain data
or error, the client SDK should treat the request as failed with
Google error code INTERNAL .
error - ....
data - The value returned by the function. This can be any valid JSON value. The firebase-functions SDK automatically encodes the value
returned by the user into this JSON format. The client SDKs
automatically decode these params into native types according to the
serialization format described below.
If other fields are present, they should be ignored.
So, to answer to your question "what should I return to the client?", you should return data that can be JSON encoded. See also this section of the protocol doc.
For example, as detailed in the doc, in a Callable Cloud you can do
return {
firstNumber: firstNumber,
secondNumber: secondNumber,
operator: '+',
operationResult: firstNumber + secondNumber,
};
//Excerpt of the doc
or, you can do
return {result: "success"}
In your specific case ("What should I return in an https callable function if it doesn't expect to return nothing") you could very well return the following, as you mentioned in your question:
const date = new Date();
const message = "the message";
return { code: "200 OK", date: date, body: message };
But you could also do return true; or return null;... It's somehow up to you to decide what makes sense in your context.
Note that in the case you return { code: "200 OK", date: date, body: message } the value of code will not be considered, by the client, as an HTTP Response code, since this JSON object is injected in the Response body.

Related

How to stop nestjs from sending 200 response before completing task in service when using google identity

I am using google-auth-library to verify the token received from gsi client on my Nestjs server. After the token has been verified, I want to send the returned object from verifyIdToken method back to the frontend as a response. I can verify the JWT token through verifyIdToken just fine but the nestjs response handler sends the 200 OK status response before I can send the returned object from verifyIdToken.
I want to send the object returned from verifyIdToken method before sending the 200 OK response.
Here's the code from google docs -
const {OAuth2Client} = require('google-auth-library');
const client = new OAuth2Client(CLIENT_ID);
async function verify() {
const ticket = await client.verifyIdToken({
idToken: token,
audience: CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
//^---------- Nestjs sends 200 OK response after this line and then continues afterwards
const payload = ticket.getPayload();
//^---------- I want to send this payload back to the frontend.
const userid = payload['sub'];
// If request specified a G Suite domain:
// const domain = payload['hd'];
}
verify().catch(console.error);
I want to send the 200 OK response along with payload variable as response body.
Also I don't really understand what verifyIdToken is trying to do other than decoding jwt. Is it trying to maintain some session? I can decode the jwt through other means and send the object back to the frontend but I don't think that's a very good idea. As it defeats the whole purpose of google sign in verification from google servers.
This is what verifyIdToken calls internally in oauth2client.js -
verifyIdToken(options, callback) {
// This function used to accept two arguments instead of an options object.
// Check the types to help users upgrade with less pain.
// This check can be removed after a 2.0 release.
if (callback && typeof callback !== 'function') {
throw new Error('This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry.');
}
if (callback) {
this.verifyIdTokenAsync(options).then(r => callback(null, r), callback);
}
else {
return this.verifyIdTokenAsync(options);
}
}
Which in return calls -
async verifyIdTokenAsync(options) {
if (!options.idToken) {
throw new Error('The verifyIdToken method requires an ID Token');
}
const response = await this.getFederatedSignonCertsAsync();
//^------- This is where the Nestjs response handler actually returns the 200 OK response prematurely
const login = await this.verifySignedJwtWithCertsAsync(options.idToken, response.certs, options.audience, OAuth2Client.ISSUERS_, options.maxExpiry);
return login;
}
I tried passing callback to verifyIdToken but the problem still remains.
Edit - This is what google docs say about verifyIdToken -
The verifyIdToken function verifies the JWT signature, the aud claim, the exp claim, and the iss claim.

Custom error message in Google Apps Script Error object [duplicate]

This is a doPost function inside a Google App that returns a Hello World message.
function doPost(e){
return ContentService.createTextOutput('Hello World');
}
Now suppose I want to only accept valid JSON to be posted to this Google App endpoint and I want to send a respones with Bad Request status. How can I do that. Here's the pseudo code:
function doPost(e){
try{
const data = JSON.parse(e.postData.contents);
return ContentService.createTextOutput('Hello World');
}catch(err){
// Send Bad Request
}
}
Issue and workaround:
Unfortunately, in the current stage, ContentService cannot modify the status code. When I saw the official document of Class ContentService, such method cannot be found. Ref It seems that this is the current specification.
So in your situation, as the current workaround, how about returning the value as JSON data? By this, you can check the value using the key of JSON data. For example, how about the following sample script?
When the correct value without the error is returned,
return ContentService.createTextOutput(JSON.stringify({value: 'value'}));
When the value with the error is returned,
return ContentService.createTextOutput(JSON.stringify({error: 'Error message'}));
When you need .setMimeType(ContentService.MimeType.JSON), please add this.
Note:
When I searched about this at the Google issue tracker, I couldn't find it. So how about reporting this as the future request? Ref
Reference:
Class ContentService
Here's another workaround that allows raising errors on the client side for errors on the web app side. For example, a client might need to catch errors such as bad url args sent to the web app (i.e. the OP's question), or catch errors thrown by a method that is called from doGet() or doPost().
As far as I know, when an error is thrown downstream of doGet() or doPost(), a text error message is returned in the response, but the web app request itself succeeds, so there is no error thrown on the client side. As #Tanaike said, there still seems no way for a Google web app dev to throw an HTTP error from the app (like 400 Bad Request or 500 Internal Server Error).
The idea involves returning a function body from the web app, which the client can use to create and run a dynamic function via the Function() constructor (this assumes Javascript is available on the client).
So the web app can be written to:
return a function body that will throw an error for bad args, server method errors, etc.
return a function body that will return intended JSON when there is no error
This is a bit of a hack, but it unifies error handling on the client side. The client makes the http request, constructs a function using the function body returned in the response, and then runs this function, all in one try{} block. Then both Google-raised http errors and web app downstream errors can be caught in the catch{} block.
Example setup for a Google Apps Script client making a request to a Google web app:
(1) In the web app doGet() or doPost() function:
// this string will be returned by the webapp
var fnBody;
// for bad url args, return a fnBody that will throw an error with an indicative message
if(!urlArgsOk()) {
fnBody = "'use strict'; throw new Error('POST args error');";
}
// if url args are ok, call server method
else {
try {
// if the method call succeeds, return a fnBody that will return the intended JSON
var returnObj = myServerMethod(methodArgs);
fnBody = "'use strict'; return JSON.stringify(" + JSON.stringify(returnObj) + ");";
}
catch(serverErr) {
// if the method call fails, return a fnBody that will throw an error ...
// ... simple example shown here, but info from serverErr can be included in fnBody
fnBody = "'use strict'; throw new Error('server error');";
}
}
// return fnBody, which can be run via Function() on the client
return ContentService.createTextOutput(fnBody).setMimeType(ContentService.MimeType.TEXT);
(2) On the client side (Google apps script client making a POST request)
// Set the url, payload, and fetch options
var url = "https://script.google.com/_______/exec?arg1=val1&arg2=val2";
var payload = getPayloadString(); // whatever POST payload needs to be sent
var options = {
'method' : 'POST',
'contentType': 'application/json',
'muteHttpExceptions': false, // let Google http exceptions come through
'payload' : payload,
'headers': {authorization: "Bearer " + ScriptApp.getOAuthToken()}
};
// Send a request to the web app
try {
// make the POST request - this throws Google-generated HTTP errors if any
var response = UrlFetchApp.fetch(url, options);
// create the dynamic function from the fnBody returned
var responseFn = new Function(response.getContentText());
// run the function - this returns intended JSON content
// or throws web app downstream errors if any
var responseJson = responseFn();
}
catch(err) {
// handle either source of error
console.log(err.message);
}
There are potential security risks associated with dynamic code, so I'm not sure how widely applicable this might be. I might use this in an aplication that lives entirely in a private GCP domain, i.e. with the web app restricted to same-domain users and the client app also in the same domain. Some security is also added by the 'use strict' directive, which boxes the dynamic function in by setting its this to undefined (ref). But it's still a good idea to think through the dynamic code implications (ref1, ref2).

How can I make a Cloudflare worker which overwrites a response status code but preserves the rest of the response?

Specifically I am interested in changing all responses with code 403 to code 404, and changing all responses with code 301 to 302. I do not want any other part of the response to change, except the status text (which I want to be empty). Below is my own attempt at this:
addEventListener("fetch", event => {
event.respondWith(fetchAndModify(event.request));
});
async function fetchAndModify(request) {
// Send the request on to the origin server.
const response = await fetch(request);
const body = await response.body
newStatus = response.status
if (response.status == 403) {
newStatus = 404
} else if (response.status == 301) {
newStatus = 302
}
// Return modified response.
return new Response(body, {
status: newStatus,
statusText: "",
headers: response.headers
});
}
I have confirmed that this code works. I would like to know if there is any possibility at all that this overwrites part of the response other than the status code or text, and if so, how can I avoid that? If this goes against certain best practices of Cloudflare workers or javascript, please describe which ones and why.
You've stumbled on a real problem with the Fetch API spec as it is written today.
As of now, status, statusText, and headers are the only standard properties of Response's init structure. However, there's no guarantee that they will remain the only properties forever, and no guarantee that an implementation doesn't provide additional non-standard or not-yet-standard properties.
In fact, Cloudflare Workers today implements a non-standard property: webSocket, which is used to implement WebSocket proxying. This property is present if the request passed to fetch() was a WebSocket initiation request and the origin server completed a WebSocket handshake. In this case, if you drop the webSocket field from the Response, WebSocket proxying will break -- which may or may not matter to you.
Unfortunately, the standard does not specify any good way to rewrite a single property of a Response without potentially dropping unanticipated properties. This differs from Request objects, which do offer a (somewhat awkward) way to do such rewrites: Request's constructor can take another Request object as the first parameter, in which case the second parameter specifies only the properties to modify. Alternately, to modify only the URL, you can pass the URL as the first parameter and a Request object as the second parameter. This works because a Request object happens to be the same "shape" as the constructor's initializer structure (it's unclear if the spec authors intended this or if it was a happy accident). Exmaples:
// change URL
request = new Request(newUrl, request);
// change method (or any other property)
request = new Request(request, {method: "GET"});
But for Response, you cannot pass an existing Response object as the first parameter to Response's constructor. There are straightforward ways to modify the body and headers:
// change response body
response = new Response(newBody, response);
// change response headers
// Making a copy of a Response object makes headers mutable.
response = new Response(response.body, response);
response.headers.set("Foo", "bar");
But if you want to modify status... well, there's a trick you can do, but it's not pretty:
// Create an initializer by copying the Response's enumerable fields
// into a new object.
let init = {...response};
// Modify it.
init.status = 404;
init.statusText = "Not Found";
// Work around a bug where `webSocket` is `null` but needs to be `undefined`.
// (Sorry, I only just noticed this when testing this answer! We'll fix this
// in the future.)
init.webSocket = init.webSocket || undefined;
// Create a new Response.
response = new Response(response.body, init);
But, ugh, that sure was ugly.
I have proposed improvements to the Fetch API to solve this, but I haven't yet had time to follow through on them. :(

Uncaught (in promise) SyntaxError: Unexpected end of JSON input

I am trying to send a new push subscription to my server but am encountering an error "Uncaught (in promise) SyntaxError: Unexpected end of JSON input" and the console says it's in my index page at line 1, which obviously is not the case.
The function where I suspect the problem occurring (because error is not thrown when I comment it out) is sendSubscriptionToBackEnd(subscription) which is called in the following:
function updateSubscriptionOnServer(subscription) {
const subscriptionJson = document.querySelector('.js-subscription-json');
const subscriptionDetails = document.querySelector('.js-subscription-details');
if (subscription) {
subscriptionJson.textContent = JSON.stringify(subscription);
sendSubscriptionToBackEnd(subscription);
subscriptionDetails.classList.remove('is-invisible');
} else {
subscriptionDetails.classList.add('is-invisible');
}
}
The function itself (which precedes the above function):
function sendSubscriptionToBackEnd(subscription) {
return fetch('/path/to/app/savesub.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
})
.then(function(response) {
if (!response.ok) {
throw new Error('Bad status code from server.');
}
return response.json();
})
.then(function(responseData) {
if (!(responseData.data && responseData.data.success)) {
throw new Error('Bad response from server.');
}
});
}
I have tried replacing single quotes with double quotes in the fetch call but that yields the same results.
I know that the JSON should be populated because it prints to the screen in the updateSubscriptionOnServer() function with subscriptionJson.textContent = JSON.stringify(subscription);, and I used that output in the google codelab's example server to receive a push successfully.
EDIT: Here is the JSON as a string, but I don't see a mistake in syntax:
{"endpoint":"https://fcm.googleapis.com/fcm/send/dLmthm1wZuc:APA91bGULRezL7SzZKywF2wiS50hXNaLqjJxJ869y8wiWLA3Y_1pHqTI458VIhJZkyOsRMO2xBS77erpmKUp-Tg0sMkYHkuUJCI8wEid1jMESeO2ExjNhNC9OS1DQT2j05BaRgckFbCN","keys":{"p256dh":"BBz2c7S5uiKR-SE2fYJrjPaxuAiFiLogxsJbl8S1A_fQrOEH4_LQjp8qocIxOFEicpcf4PHZksAtA8zKJG9pMzs=","auth":"VOHh5P-1ZTupRXTMs4VhlQ=="}}
Any ideas??
This might be a problem with the endpoint not passing the appropriate parameters in the response's header.
In Chrome's console, inside the Network tab, check the headers sent by the endpoint and it should contain this:
Example of proper response to allow requests from localhost and cross domains requests
Ask the API developer to include this in the headers:
"Access-Control-Allow-Origin" : "*",
"Access-Control-Allow-Credentials" : true
This happened to me also when I was running a server with Express.js and using Brave browser. In my case it was the CORs problem. I did the following and it solved the problem in my case:
(since this is an Express framework, I am using app.get)
-on the server side:
res.set({
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
});
-on client side I used Fetch to get data but disabled the CORS option
// mode: "no-cors" //disabled this in Fetch
That took care of my issues with fetching data with Express
This can be because you're not sending any JSON from the server
OR
This can be because you're sending invalid JSON.
Your code might look like
res.end();
One of the pitfalls is that returned data that is not a JSON but just a plain text payload regardless of headers set. I.e. sending out in Express via something like
res.send({a: "b"});
rather than
res.json({a: "b"});
would return this confusing error. Not easy to detect in network activity as it looks quite legit.
For someone looking here later. I received this error not because of my headers but because I was not recursively appending the response body to a string to JSON.parse later.
As per the MDN example (I've taken out some parts of their example not immediately relevant):
reader.read().then(function processText({ done, value }) {
if (done) {
console.log("Stream complete");
return;
}
result += chunk;
return reader.read().then(processText);
});
For my issue I had to
Use a named function (not an anonymous ()=>{}) inside the .then
Append the result together recursively.
Once done is true execute something else on the total appended result
Just in case this is helpful for you in the future and your issue is not header related, but related to the done value not being true with the initial JSON stream response.
I know this question has already been answered but just thought I add my thoughts.
This will happen when your response body is empty and response.json() is expecting a JSON string. Make sure that your API is returning a response body in JSON format if must be.

AngularJS: Cancel overwriting values $resource object after calling save ();

var User = $resource(
'http://test/index.php'
);
var user = User.get({id:'1'});
// GET: http://test/index.php?id=1
// server returns: { "login":"foo", "name":"bar", "mail":"baz" }
user.name = "qux";
user.$save();
// POST: http://test/index.php?id=1
// server returns: { "login":"foo", "name":"bar", "mail":"qux"}
In this case, when you call the save() user object, properties will be replaced by those that came from the server.
But if the server responds like this:
{
"errors":{
"login":"too short",
"name":"is already using that name.",
"mail":"invalid email."
}
}
User object properties are overwritten and instead, property errors containing these mistakes will come up.
Is there a way to change the behavior of $resource? I would like to check the status of the response and, based on that, decide whether to update the properties of an object or report an error to the user.
Angular's $resource is meant to interact with RESTful web services.
In RESTful web services, if there's an error while saving a resource, you should return an appropriate HTTP status (for example, 400).
Then, you can optionally use the error callback:
user.$save(function (response) {
console.log("success!");
}, function (response) {
console.log("error");
});
For a full list of error HTTP statuses:
http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error

Categories