How to check lowercased URLs and redirect if they're valid with Lambda#Edge - javascript

I've got some old URLs that used to use capital letters for people's names and such, but now I've updated my site to just lowercase characters in the URLs of every page. So, I'd like to redirect people if they happen to have clicked on an old link, or typed a capital letter by accident.
I also check removing a trailing slash. Here's the code I currently use on the front end. I was hoping to switch over to using Lambda#Edge (My website is on S3 and distributed via CloudFront) for that check and redirect.
Here's the JS function I'm using on the front end:
var newUrl = window.location.href.toLowerCase().replace(/\/$/, '')
loadIfChanged(newUrl)
function loadIfChanged (newUrl) {
if (newUrl != location.href) {
fetch(newUrl, {method: 'HEAD'}).then(function (response) {
if (response.status === 200) return window.location = newUrl
}).catch(function (error) {
return console.log(error)
})
}
}
How might I write that in a Lambda#Edge function?
Maybe something like this:
exports.handler = async function (event, context) {
// Lowercase the URL and trim off a trailing slash if there is one
var path = event.Records[0].cf.request.uri.toLowerCase().replace(/\/$/, '')
// How to do a fetch here? var ok = fetch()
if (ok) {
const response = {
status: '301',
statusDescription: 'Moved Permanently',
headers: {
location: [{
key: 'Location',
value: `https://example.com/${path}`,
}],
},
}
return response
} else {
return event.Records[0].cf.request
}
}
Can Lambda#Edge functions even do I/O?
And, importantly, can this Lambda#Edge function run only on 404s?

What you wrote should work, but I think better implementation would be to simply change the URI in the OriginRequest Lambda function. The code is as simple as :
import json
def lambda_handler(event, context):
request = event['Records'][0]['cf']['request'];
request['uri'] = "anything";
return request;
An interesting detail to note here is that CF will cache the result of modified URI against the original URI. So if a request comes again for the original URI, CloudFront will return the response directly from the cache.
Why Origin Request?
Because it is triggered only on cache misses
It helps get the benefit of cache described above
Why this way is better as compared to what you proposed?
If you decide to go via the above mentioned approach, following is the request flow:
For non-cached content:
User -> CF -> Lambda -> S3 -> Final response to the user
For cached content:
User -> CF -> Final response to the user
Whereas if you use Lambda to generate 301 the flow would be:
For non-cached content:
User -> CF -> Lambda -> User -> CF -> Lambda -> S3 -> Final response to the user
For cached content:
User -> CF(cf returns 301) -> User -> CF -> Final response to the user
So you get two advantages in this approach :
Lesser latency for your user.
Lesser lambda invocation which means lesser bill for you.
For your second question, I do not think there is any way to invoke Lambda#Edge only for 404s as of now.
Hope this helps!

Related

How to read a response in Cypress?

I am trying to write an E2E test using Cypress 12.3 for a web application. During a certain part of the journey, the app makes a GET request to the endpoint "api/v2/accountApplication/getApplicationInfo?uuid=xxxxxxx". The response from this request includes a field called "abChoice.welcome" which has a value of either 'a' or 'b'. This value is used for A/B testing in my Vue app. The structure of the response is as follows:
{
"resultStatus": true,
"errorCode": 0,
"errorMessage": null,
"resultData": {
"abChoice": {
"welcome": "a"
}
}
}
I am trying to write a test that checks the response from this request and makes different assertions based on the value of "abChoice.welcome". I have attempted to use the cy.intercept command to check the response, but it is not working as expected. I also tried creating an alias for the request and using cy.wait(#myAliasName), but Cypress threw an error and said the request was never made, even though I can see the request in the logs.
describe('A/B testing', () => {
it('shows A or B pages', () => {
cy.intercept('GET', '**/accountApplication/getApplicationInfo', req => {
const { body } = req
cy.log(body)
if (body.resultData.abChoice.wlecome === 'a') {
cy.log('A')
// assert something
} else {
cy.log('B')
// assert something
}
})
})
})
The log shows the following, so the request is definitely being made. (I've removed sensitive information)
(xhr)GET 200 /api/v2/xxxx/accountApplication/getApplicationInfo?uuid=xxxx
You may have the same issue as here Cypress intercept() fails when the network call has parameters with '/'.
In fact it's not "/" that causes the issue but the parameter section (anything after ?), it would be considered as part of the URL.
As per reference question, use a regex or pathname instead of url (default).
cy.intercept('GET', /\/accountApplication\/getApplicationInfo/, req => {
or with pathname, parameters are excluded from the match
cy.intercept({method:'GET', pathname: `**/accountApplication/getApplicationInfo`}, req => {
The reason your cy.wait() does not succeed and your validation is never run is because your intercept's request url is never matched. This is because your url does not include the query parameters.
Assuming that the query parameter uuid is not relevant to your intercept, you could do something like the following, adding a wildcard onto the end of your existing url:
cy.intercept('GET', '**/accountApplication/getApplicationInfo*', (req) => { ... });
If uuid were relevant, you could simply attach it to the end of url matched on:
cy.intercept('GET', '**/accountApplication/getApplicationInfo?uuid=*', (req) => { ... });
If you weren't assured that uuid was going to be the first query parameter, you would need to update the "simple-string" url method to using a RouteMatcher.
cy.intercept({
pathname: '**/accountApplication/getApplicationInfo'
query: {
uuid: 'foo'
},
method: 'GET'
}, (req) => { ... });

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

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.

Cannot construct a Request with a Request whose mode is 'navigate' and a non-empty RequestInit

Consider this sample index.html file.
<!DOCTYPE html>
<html><head><title>test page</title>
<script>navigator.serviceWorker.register('sw.js');</script>
</head>
<body>
<p>test page</p>
</body>
</html>
Using this Service Worker, designed to load from the cache, then fallback to the network if necessary.
cacheFirst = (request) => {
var mycache;
return caches.open('mycache')
.then(cache => {
mycache = cache;
cache.match(request);
})
.then(match => match || fetch(request, {credentials: 'include'}))
.then(response => {
mycache.put(request, response.clone());
return response;
})
}
addEventListener('fetch', event => event.respondWith(cacheFirst(event.request)));
This fails badly on Chrome 62. Refreshing the HTML fails to load in the browser at all, with a "This site can't be reached" error; I have to shift refresh to get out of this broken state. In the console, it says:
Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'ServiceWorkerGlobalScope': Cannot construct a Request with a Request whose mode is 'navigate' and a non-empty RequestInit.
"construct a Request"?! I'm not constructing a request. I'm using the event's request, unmodified. What am I doing wrong here?
Based on further research, it turns out that I am constructing a Request when I fetch(request, {credentials: 'include'})!
Whenever you pass an options object to fetch, that object is the RequestInit, and it creates a new Request object when you do that. And, uh, apparently you can't ask fetch() to create a new Request in navigate mode and a non-empty RequestInit for some reason.
In my case, the event's navigation Request already allowed credentials, so the fix is to convert fetch(request, {credentials: 'include'}) into fetch(request).
I was fooled into thinking I needed {credentials: 'include'} due to this Google documentation article.
When you use fetch, by default, requests won't contain credentials such as cookies. If you want credentials, instead call:
fetch(url, {
credentials: 'include'
})
That's only true if you pass fetch a URL, as they do in the code sample. If you have a Request object on hand, as we normally do in a Service Worker, the Request knows whether it wants to use credentials or not, so fetch(request) will use credentials normally.
https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker
var networkDataReceived = false;
// fetch fresh data
var networkUpdate = fetch('/data.json').then(function(response) {
return response.json();
}).then(function(data) {
networkDataReceived = true;
updatePage(data);
});
// fetch cached data
caches.match('mycache').then(function(response) {
if (!response) throw Error("No data");
return response.json();
}).then(function(data) {
// don't overwrite newer network data
if (!networkDataReceived) {
updatePage(data);
}
}).catch(function() {
// we didn't get cached data, the network is our last hope:
return networkUpdate;
}).catch(showErrorMessage).then(console.log('error');
Best example of what you are trying to do, though you have to update your code accordingly. The web example is taken from under Cache then network.
for the service worker:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('mycache').then(function(cache) {
return fetch(event.request).then(function(response) {
cache.put(event.request, response.clone());
return response;
});
})
);
});
Problem
I came across this problem when trying to override fetch for all kinds of different assets. navigate mode was set for the initial Request that gets the index.html (or other html) file; and I wanted the same caching rules applied to it as I wanted to several other static assets.
Here are the two things I wanted to be able to accomplish:
When fetching static assets, I want to sometimes be able to override the url, meaning I want something like: fetch(new Request(newUrl))
At the same time, I want them to be fetched just as the sender intended; meaning I want to set second argument of fetch (i.e. the RequestInit object mentioned in the error message) to the originalRequest itself, like so: fetch(new Request(newUrl), originalRequest)
However the second part is not possible for requests in navigate mode (i.e. the initial html file); at the same time it is not needed, as explained by others, since it will already keep it's cookies, credentials etc.
Solution
Here is my work-around: a versatile fetch that...
can override the URL
can override RequestInit config object
works with both, navigate as well as any other requests
function fetchOverride(originalRequest, newUrl) {
const fetchArgs = [new Request(newUrl)];
if (request.mode !== 'navigate') {
// customize the request only if NOT in navigate mode
// (since in "navigate" that is not allowed)
fetchArgs.push(request);
}
return fetch(...fetchArgs);
}
In my case I was contructing a request from a serialized form in a service worker (to handle failed POSTs). In the original request it had the mode attribute set, which is readonly, so before one reconstructs the request, delete the mode attribute:
delete serializedRequest["mode"];
request = new Request(serializedRequest.url, serializedRequest);

Meteor: what is the final Location/URL after following redirects?

Meteor's HTTP package is a wrapper around mikeal's request, and it supports the followRedirects option. But how can one find out what the final URL is, after the 3xx redirect responses have been followed (and the request didn't fail because of lack of a cookie jar)?
With request, the final URL is in response.request.href. But with Meteor... ?
Here's the Meteor code:
if (Meteor.isServer) {
Meteor.startup(function () {
var url = 'http://google.com';
var result = HTTP.call("HEAD", url, {
followRedirects: true
});
console.log(result); // nothing here hints at the final URL
});
}
I've created a package that does this - http-more.
Turns out Meteor doesn't pass back the request object within the response, and given the history of rejected PRs concerning enhancements to the HTTP package, I've just implemented that option separately.

ExtJS 4.1 or general Javascript HTTP_Authorization Function

When making a request using curl --basic --user someuser:somepass http://someurl/ it append a header like this: Authorization: Basic Y2FsaWRvZzpmMDBmM2IwMg==. This is, of course,
Basic access authentication is a method for a web browser or other client program to provide a user name and password when making a request. Before transmission, the user name is appended with a colon and concatenated with the password. The resulting string is encoded with the Base64 algorithm and transmitted in the HTTP header and decoded by the receiver, resulting in the colon-separated user name and password string. Basic access authentication was originally defined in 1996 by RFC 1945 [http://en.wikipedia.org/wiki/Basic_access_authentication][1] [1]: http://en.wikipedia.org/wiki/Basic_access_authentication
I'm looking for a way to have all my ExtJS 4.1 Ext.data.proxy.Rest proxies add this to all requests. It seems like a simple task, but I can find no documentation on it. By the way, I do know how to add headers to proxies generally through headers: {'X_MyHeader':'somevalue'} property. I just do not know how to tell Ext to do it globally for the current username/password.
I was looking for a similar solution for applying OAuth tokens to my Ext.data.proxy.Rest proxies and couldn't find any info about it. Quite shocked there is nothing out there about how to do this. Makes me think I'm going about it the wrong way. Anyway heres what I've come up with to achieve this.
Override Ext.data.proxy.Ajax and keep a static variable so it applies to all proxies. Then merge the set authHeader with the current list of headers so you can still customise per proxy.
Ext.override(Ext.data.proxy.Ajax, {
statics: {
authHeader: undefined,
},
/**
* #cfg {Object} headers
* Any headers to add to the Ajax request. Defaults to undefined.
*/
doRequest: function(operation, callback, scope) {
var writer = this.getWriter(),
request = this.buildRequest(operation, callback, scope);
if (operation.allowWrite()) {
request = writer.write(request);
}
console.log(this.headers);
Ext.apply(request, {
headers : Ext.apply(this.headers, Ext.data.proxy.Ajax.authHeader) || Ext.data.proxy.Ajax.authHeader,
timeout : this.timeout,
scope : this,
callback : this.createRequestCallback(request, operation, callback, scope),
method : this.getMethod(request),
disableCaching: false // explicitly set it to false, ServerProxy handles caching
});
Ext.Ajax.request(request);
return request;
}
});
Then I look for my token in local storage, if it's there set it globally, otherwise prompt for a login.
MyApp.model.AuthToken.load(1, {
callback: function(record) {
// If we don't have anything in local storage for the user, show the login box.
if (record.get('access_token') == '') {
var window = Ext.create('MyApp.view.Login');
window.show();
} else {
Ext.data.proxy.Ajax.authHeader = { 'Authorization': 'Bearer ' + record.get('access_token') };
}
}
});
There should be some extra logic in there for handling token expiry and refresh requests, but you get the idea!

Categories