Skip to content

Commit 2575b70

Browse files
authored
Support redirects in preview (#1270)
This add support for parsing the redirects.conf file that we've been including in every do build for a while. It is *technically* an nginx config file but it is safe to treat it as a list of regex substitutions.
1 parent 59110dc commit 2575b70

File tree

7 files changed

+101
-14
lines changed

7 files changed

+101
-14
lines changed

integtest/spec/all_books_spec.rb

+4-3
Original file line numberDiff line numberDiff line change
@@ -449,9 +449,10 @@ def self.setup_example(repo, lang, hash)
449449
HTML
450450
end
451451
it 'serves a legacy redirect' do
452-
expect(legacy_redirect.code).to eq('301')
453-
expect(legacy_redirect['location']).to eq(
454-
"#{root}/en/elasticsearch/reference/current/setup.html"
452+
expect(legacy_redirect).to redirect_to(
453+
eq(
454+
"#{root}/en/elasticsearch/reference/current/setup.html"
455+
)
455456
)
456457
end
457458
context 'for a JPG' do

integtest/spec/helper/dsl.rb

+3-5
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,10 @@ def dest_file(file)
4646

4747
RSpec.shared_examples 'the root' do
4848
context 'the root' do
49-
let(:root) do
50-
Net::HTTP.get_response(URI('http://localhost:8000/'))
51-
end
49+
let(:root_uri) { 'http://localhost:8000' }
50+
let(:root) { Net::HTTP.get_response(URI(root_uri)) }
5251
it 'redirects to the guide root' do
53-
expect(root.code).to eq('301')
54-
expect(root['Location']).to eq('http://localhost:8000/guide/index.html')
52+
expect(root).to redirect_to(eq("#{root_uri}/guide/index.html"))
5553
end
5654
end
5755
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
##
4+
# Matches http responses.
5+
RSpec::Matchers.define :redirect_to do |expected|
6+
match do |actual|
7+
return false unless actual.code == '301'
8+
9+
expected.matches? actual['Location']
10+
end
11+
failure_message do |actual|
12+
unless actual.code == '301'
13+
return "expected status [301] but was [#{actual.code}]. Body:\n" +
14+
actual.body
15+
end
16+
17+
message = expected.failure_message
18+
"status was [301] but the location didn't match: #{message}"
19+
end
20+
end

integtest/spec/preview_spec.rb

+20-4
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ def get(watermark, branch, path)
9292
let(:directory) do
9393
get watermark, branch, 'guide'
9494
end
95+
let(:legacy_redirect) do
96+
get watermark, branch, 'guide/reference/setup/'
97+
end
9598
end
9699

97100
let(:expected_js_state) { {} }
@@ -105,8 +108,7 @@ def get(watermark, branch, path)
105108
shared_examples 'serves some docs' do |supports_gapped: true|
106109
context 'the root' do
107110
it 'redirects to the guide root' do
108-
expect(root.code).to eq('301')
109-
expect(root['Location']).to eq("http://#{host}:8000/guide/index.html")
111+
expect(root).to redirect_to(eq("http://#{host}:8000/guide/index.html"))
110112
end
111113
it 'logs the access to the docs root' do
112114
wait_for_access '/'
@@ -177,6 +179,21 @@ def get(watermark, branch, path)
177179
TXT
178180
expect(robots_txt['Content-Type']).to eq('text/plain')
179181
end
182+
context 'for a legacy redirect' do
183+
it 'serves the redirect' do
184+
expect(legacy_redirect).to redirect_to(
185+
eq(
186+
'/guide/en/elasticsearch/reference/current/setup.html'
187+
)
188+
)
189+
end
190+
it 'logs the access to the legacy redirect' do
191+
wait_for_access '/guide/reference/setup/'
192+
expect(logs).to include(<<~LOGS)
193+
#{watermark} #{host} GET /guide/reference/setup/ HTTP/1.1 301
194+
LOGS
195+
end
196+
end
180197
end
181198
shared_examples '404s' do
182199
it '404s for the docs root' do
@@ -252,8 +269,7 @@ def get(watermark, branch, path)
252269
end
253270
context 'when you request a directory' do
254271
it 'redirects to index.html' do
255-
expect(directory.code).to eq('301')
256-
expect(directory['Location']).to eq('/guide/index.html')
272+
expect(directory).to redirect_to(eq('/guide/index.html'))
257273
end
258274
end
259275
end

integtest/spec/spec_helper.rb

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require_relative 'helper/matcher/doc_body'
44
require_relative 'helper/matcher/have_same_keys'
55
require_relative 'helper/matcher/initial_js_state'
6+
require_relative 'helper/matcher/redirect_to'
67
require_relative 'helper/matcher/serve'
78
require_relative 'helper/console_alternative_examples'
89
require_relative 'helper/dest'

preview/core.js

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const GitCore = (defaultTemplate, ignoreHost, repoPath) => {
5252

5353
return {
5454
diff: () => Readable.from(bufferItr(diffItr(git, branch), 16 * 1024)),
55+
redirects: () => git.catBlob(`${branch}:redirects.conf`),
5556
file: async requestedPath => {
5657
const templateExists = await hasTemplate(git, branch);
5758
/*
@@ -113,6 +114,7 @@ const FsCore = (defaultTemplate, ignoreHost, rootPath) => {
113114
r.push(null);
114115
return r;
115116
},
117+
redirects: () => null,
116118
file: async requestedPath => {
117119
const realPath = `${rootPath}/${requestedPath}`;
118120
let pathStat;

preview/preview.js

+51-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,15 @@ const requestHandler = async (core, parsedUrl, response) => {
4040
}
4141

4242
const path = parsedUrl.pathname.substring("/guide".length);
43-
const type = contentType(path);
44-
const file = await core.file(path, type);
43+
const redirect = await checkRedirects(core, path);
44+
if (redirect) {
45+
response.statusCode = 301;
46+
response.setHeader('Location', redirect);
47+
response.end();
48+
return;
49+
}
50+
51+
const file = await core.file(path);
4552
if (file === "dir") {
4653
response.statusCode = 301;
4754
const sep = parsedUrl.pathname.endsWith('/') ? '' : '/';
@@ -55,6 +62,7 @@ const requestHandler = async (core, parsedUrl, response) => {
5562
return;
5663
}
5764

65+
const type = contentType(path);
5866
response.setHeader('Content-Type', type);
5967
if (file.hasTemplate && !path.endsWith("toc.html") && type === "text/html; charset=utf-8") {
6068
const template = Template(file.template);
@@ -126,6 +134,47 @@ const hostPrefix = host => {
126134
return host.substring(0, dot);
127135
};
128136

137+
const checkRedirects = async (core, path) => {
138+
/*
139+
* This parses the nginx redirects.conf file we have in the built docs and
140+
* performs the redirects. It makes no effort to properly emulate nginx. It
141+
* just runs the regexes from start to finish. Which is fine becaues of the
142+
* redirects that we have. But it is ugly.
143+
*
144+
* It also doesen't make any effort to be fast or efficient, buffering the
145+
* entire file into memory then splitting it into lines and compiling all of
146+
* the regexes on the fly. We can absolutely do better. But this feels like
147+
* a fine place to start.
148+
*/
149+
// TODO Rebuild redirects file without nginx stuff. And stream it properly.
150+
let target = "/guide" + path;
151+
const redirectsStart = Date.now();
152+
const streamToString = stream => {
153+
const chunks = []
154+
return new Promise((resolve, reject) => {
155+
stream.on('data', chunk => chunks.push(chunk));
156+
stream.on('error', reject);
157+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
158+
});
159+
}
160+
const redirectsStream = await core.redirects();
161+
if (!redirectsStream) {
162+
// If we don't have the redirects file we skip redirects.
163+
return;
164+
}
165+
const redirectsString = await streamToString(redirectsStream);
166+
for (const line of redirectsString.split('\n')) {
167+
if (!line.startsWith("rewrite")) {
168+
continue;
169+
}
170+
const [_marker, regexText, replacement] = line.split(' ');
171+
const regex = new RegExp(regexText.replace('(?i)', ''), 'i');
172+
target = target.replace(regex, replacement);
173+
}
174+
console.log("took", Date.now() - redirectsStart);
175+
return "/guide" + path === target ? null : target;
176+
}
177+
129178
module.exports = Core => {
130179
const server = http.createServer((request, response) => {
131180
const parsedUrl = url.parse(request.url);

0 commit comments

Comments
 (0)