From 66b0652c96220e1fa2f7cb88b7023af83264241b Mon Sep 17 00:00:00 2001 From: Volodymyr Makukha Date: Thu, 15 Apr 2021 15:00:33 +0300 Subject: [PATCH 1/2] feat: support a few domains (500 template) --- ilc/common/wrapWithCache.js | 3 +- ilc/server/app.js | 3 +- ilc/server/errorHandler/ErrorHandler.js | 3 +- ilc/server/registry/Registry.js | 43 ++++++++++++++- registry/client/src/routerDomains/Edit.js | 9 +++- registry/client/src/routerDomains/List.js | 1 + registry/server/app.ts | 2 +- .../20210405164831_router_domains.ts | 1 + .../server/routerDomains/interfaces/index.ts | 12 +++-- registry/server/routerDomains/routes/index.ts | 18 ++++--- registry/tests/routerDomains.spec.ts | 54 +++++++++++++++++-- registry/tests/templates.spec.ts | 32 +++++++++++ 12 files changed, 160 insertions(+), 21 deletions(-) diff --git a/ilc/common/wrapWithCache.js b/ilc/common/wrapWithCache.js index 06f0c44a..ed1213e7 100644 --- a/ilc/common/wrapWithCache.js +++ b/ilc/common/wrapWithCache.js @@ -6,13 +6,14 @@ errors.WrapWithCacheError = extendError('WrapWithCacheError'); const wrapWithCache = (localStorage, logger, createHash = hashFn) => (fn, cacheParams = {}) => { const { cacheForSeconds = 60, + name = '', // "hash" of returned value is based only on arguments, so with the help "name" we can add prefix to hash } = cacheParams; const cacheResolutionPromise = {}; return (...args) => { const now = Math.floor(Date.now() / 1000); - const hash = args.length > 0 ? createHash(JSON.stringify(args)) : '__null__'; + const hash = `${name ? name + '__' : ''}${args.length > 0 ? createHash(JSON.stringify(args)) : '__null__'}`; if (localStorage.getItem(hash) === null || JSON.parse(localStorage.getItem(hash)).cachedAt < now - cacheForSeconds) { if (cacheResolutionPromise[hash] !== undefined) { diff --git a/ilc/server/app.js b/ilc/server/app.js index 800caf14..e7318f49 100644 --- a/ilc/server/app.js +++ b/ilc/server/app.js @@ -43,7 +43,8 @@ module.exports = (registryService, pluginManager) => { app.register(require('./ping')); app.get('/_ilc/api/v1/registry/template/:templateName', async (req, res) => { - const data = await registryService.getTemplate(req.params.templateName); + const currentDomain = req.hostname; + const data = await registryService.getTemplate(req.params.templateName, currentDomain); res.status(200).send(data.data.content); }); diff --git a/ilc/server/errorHandler/ErrorHandler.js b/ilc/server/errorHandler/ErrorHandler.js index faf8c0cb..461526e4 100644 --- a/ilc/server/errorHandler/ErrorHandler.js +++ b/ilc/server/errorHandler/ErrorHandler.js @@ -51,7 +51,8 @@ module.exports = class ErrorHandler { errorId }); - let data = await this.#registryService.getTemplate('500'); + const currentDomain = req.hostname; + let data = await this.#registryService.getTemplate('500', currentDomain); data = data.data.content.replace('%ERRORID%', `Error ID: ${errorId}`); nres.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); diff --git a/ilc/server/registry/Registry.js b/ilc/server/registry/Registry.js index c6c1a03c..9e878e37 100644 --- a/ilc/server/registry/Registry.js +++ b/ilc/server/registry/Registry.js @@ -13,6 +13,7 @@ module.exports = class Registry { #cacheHeated = { config: false, template: false, + routerDomains: false, }; /** @@ -30,6 +31,7 @@ module.exports = class Registry { const getConfigMemo = wrapFetchWithCache(this.#getConfig, { cacheForSeconds: 5, + name: 'registry_getConfig', }); this.getConfig = async (options) => { @@ -38,13 +40,28 @@ module.exports = class Registry { return res; }; - this.getTemplate = wrapFetchWithCache(this.#getTemplate, { + this.getRouterDomains = wrapFetchWithCache(this.#getRouterDomains, { cacheForSeconds: 30, + name: 'registry_routerDomains', }); + + const getTemplateMemo = wrapFetchWithCache(this.#getTemplate, { + cacheForSeconds: 30, + }); + + this.getTemplate = async (templateName, forDomain) => { + if (templateName === '500' && forDomain) { + const routerDomains = await this.getRouterDomains(); + const redefined500 = routerDomains.data.find(item => item.domainName === forDomain)?.template500; + templateName = redefined500 || templateName; + } + + return await getTemplateMemo(templateName); + }; } async preheat() { - if (this.#cacheHeated.template && this.#cacheHeated.config) { + if (this.#cacheHeated.template && this.#cacheHeated.config && this.#cacheHeated.routerDomains) { return; } @@ -53,6 +70,7 @@ module.exports = class Registry { await Promise.all([ this.getConfig(), this.getTemplate('500'), + this.getRouterDomains(), ]); this.#logger.info('Registry preheated successfully!'); @@ -102,6 +120,27 @@ module.exports = class Registry { return res.data; }; + #getRouterDomains = async () => { + this.#logger.debug('Calling get routerDomains registry endpoint...'); + + const url = urljoin(this.#address, 'api/v1/router_domains'); + let res; + try { + res = await axios.get(url, { responseType: 'json' }); + } catch (e) { + throw new errors.RegistryError({ + message: `Error while requesting routerDomains from registry`, + cause: e, + data: { + requestedUrl: url + } + }); + } + + this.#cacheHeated.routerDomains = true; + return res.data; + }; + #filterConfig = (config, filter) => { if (!filter || !Object.keys(filter).length) { return config; diff --git a/registry/client/src/routerDomains/Edit.js b/registry/client/src/routerDomains/Edit.js index cd0124b7..196ae939 100644 --- a/registry/client/src/routerDomains/Edit.js +++ b/registry/client/src/routerDomains/Edit.js @@ -6,6 +6,8 @@ import { TextInput, required, TextField, + ReferenceInput, + SelectInput, } from 'react-admin'; // eslint-disable-line import/no-unresolved const Title = ({ record }) => { @@ -19,7 +21,12 @@ const InputForm = ({ mode = 'edit', ...props }) => { ? : null} - + + + + ); }; diff --git a/registry/client/src/routerDomains/List.js b/registry/client/src/routerDomains/List.js index 377ee4f0..9a7e7652 100644 --- a/registry/client/src/routerDomains/List.js +++ b/registry/client/src/routerDomains/List.js @@ -51,6 +51,7 @@ const PostList = props => { + diff --git a/registry/server/app.ts b/registry/server/app.ts index fa70b3ae..cd069b03 100644 --- a/registry/server/app.ts +++ b/registry/server/app.ts @@ -40,7 +40,7 @@ export default (withAuth: boolean = true) => { app.use('/api/v1/auth_entities', authMw, routes.authEntities); app.use('/api/v1/versioning', authMw, routes.versioning); app.use('/api/v1/settings', routes.settings(authMw)); - app.use('/api/v1/router_domains', authMw, routes.routerDomains); + app.use('/api/v1/router_domains', routes.routerDomains(authMw)); app.use(errorHandler); diff --git a/registry/server/migrations/20210405164831_router_domains.ts b/registry/server/migrations/20210405164831_router_domains.ts index 45d73f12..6817ae88 100644 --- a/registry/server/migrations/20210405164831_router_domains.ts +++ b/registry/server/migrations/20210405164831_router_domains.ts @@ -5,6 +5,7 @@ export async function up(knex: Knex): Promise { return knex.schema.createTable('router_domains', table => { table.increments('id'); table.string('domainName', 255).notNullable(); + table.string('template500', 50).nullable().references('templates.name') }); } diff --git a/registry/server/routerDomains/interfaces/index.ts b/registry/server/routerDomains/interfaces/index.ts index 5a1866e3..0e6c28f3 100644 --- a/registry/server/routerDomains/interfaces/index.ts +++ b/registry/server/routerDomains/interfaces/index.ts @@ -1,18 +1,24 @@ import Joi from 'joi'; +import { templateNameSchema } from '../../templates/interfaces'; export default interface RouterDomains { id: number, domainName: string, + template500?: string, }; export const routerDomainIdSchema = Joi.string().trim().required(); -const routerDomainNameSchema = Joi.string().trim().min(1); +const commonRouterDomainsSchema = { + domainName: Joi.string().trim().min(1), + template500: templateNameSchema.allow(null), +}; export const partialRouterDomainsSchema = Joi.object({ - domainName: routerDomainNameSchema, + ...commonRouterDomainsSchema, }); export const routerDomainsSchema = Joi.object({ - domainName: routerDomainNameSchema.required(), + ...commonRouterDomainsSchema, + domainName: commonRouterDomainsSchema.domainName.required(), }); diff --git a/registry/server/routerDomains/routes/index.ts b/registry/server/routerDomains/routes/index.ts index 75c23721..e0a355d7 100644 --- a/registry/server/routerDomains/routes/index.ts +++ b/registry/server/routerDomains/routes/index.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import express, { RequestHandler } from 'express'; import getRouterDomains from './getRouterDomains'; import getAllRouterDomains from './getAllRouterDomains'; @@ -6,12 +6,14 @@ import updateRouterDomains from './updateRouterDomains'; import createRouterDomains from './createRouterDomains'; import deleteRouterDomains from './deleteRouterDomains'; -const routerDomainsRouter = express.Router(); +export default (authMw: RequestHandler) => { + const routerDomainsRouter = express.Router(); -routerDomainsRouter.get('/', ...getAllRouterDomains); -routerDomainsRouter.post('/', ...createRouterDomains); -routerDomainsRouter.get('/:id', ...getRouterDomains); -routerDomainsRouter.put('/:id', ...updateRouterDomains); -routerDomainsRouter.delete('/:id', ...deleteRouterDomains); + routerDomainsRouter.get('/', ...getAllRouterDomains); + routerDomainsRouter.post('/', authMw, ...createRouterDomains); + routerDomainsRouter.get('/:id', authMw, ...getRouterDomains); + routerDomainsRouter.put('/:id', authMw, ...updateRouterDomains); + routerDomainsRouter.delete('/:id', authMw, ...deleteRouterDomains); -export default routerDomainsRouter; + return routerDomainsRouter; +}; diff --git a/registry/tests/routerDomains.spec.ts b/registry/tests/routerDomains.spec.ts index 3c332fc4..582c1a77 100644 --- a/registry/tests/routerDomains.spec.ts +++ b/registry/tests/routerDomains.spec.ts @@ -29,6 +29,17 @@ describe(`Tests ${example.url}`, () => { .expect(422, '"domainName" must be a string'); }); + it('should not create record with non-existed template500', async () => { + const response = await request.post(example.url) + .send({ + ...example.correct, + template500: 'nonExistedTemplate', + }) + .expect(500); + + expect(response.text).to.include('Internal server error occurred.'); + }); + it('should successfully create record', async () => { let routerDomainsId; @@ -56,6 +67,46 @@ describe(`Tests ${example.url}`, () => { } }); + it('should successfully create record with template for 500', async () => { + const templateName = 'testTemplate500'; + let routerDomainsId; + + const exampleWithTemplate500 = { + ...example.correct, + template500: templateName, + }; + + try { + await request.post('/api/v1/template/') + .send({ + name: templateName, + content: 'ncTestTemplateContent' + }); + + const responseCreation = await request.post(example.url) + .send(exampleWithTemplate500) + .expect(200) + + routerDomainsId = responseCreation.body.id; + + expect(responseCreation.body).deep.equal({ + id: routerDomainsId, + ...exampleWithTemplate500, + }); + + const responseFetching = await request.get(example.url + routerDomainsId) + .expect(200); + + expect(responseFetching.body).deep.equal({ + id: routerDomainsId, + ...exampleWithTemplate500, + }); + } finally { + routerDomainsId && await request.delete(example.url + routerDomainsId); + await request.delete('/api/v1/template/' + templateName); + } + }); + describe('Authentication / Authorization', () => { it('should deny access w/o authentication', async () => { await requestWithAuth.post(example.url) @@ -159,9 +210,6 @@ describe(`Tests ${example.url}`, () => { describe('Authentication / Authorization', () => { it('should deny access w/o authentication', async () => { - await requestWithAuth.get(example.url) - .expect(401); - await requestWithAuth.get(example.url + 123) .expect(401); }); diff --git a/registry/tests/templates.spec.ts b/registry/tests/templates.spec.ts index 5ff56ae9..d58e81dc 100644 --- a/registry/tests/templates.spec.ts +++ b/registry/tests/templates.spec.ts @@ -292,6 +292,38 @@ describe(`Tests ${example.url}`, () => { expect(response.body).deep.equal({}); }); + it('should successfully delete record if doesn\'t have any reference from foreign (routerDomains -> template500) to current primary key', async () => { + let routerDomainsId; + + try { + await request.post(example.url).send(example.correct).expect(200); + + const responseRouterDomains = await request.post('/api/v1/router_domains/') + .send({ + domainName: 'domainNameCorrect', + template500: example.correct.name, + }) + .expect(200) + + routerDomainsId = responseRouterDomains.body.id; + + const response = await request.delete(example.url + example.correct.name) + .expect(500); + expect(response.text).to.include('Internal server error occurred.'); + + await request.delete('/api/v1/router_domains/' + routerDomainsId); + + await request.delete(example.url + example.correct.name) + .expect(204, ''); + + await request.get(example.url + example.correct.name) + .expect(404, 'Not found'); + } finally { + routerDomainsId && await request.delete('/api/v1/router_domains/' + routerDomainsId); + await request.delete(example.url + example.correct.name) + } + }); + it('should successfully delete record', async () => { await request.post(example.url).send(example.correct).expect(200); From b2ce5be8c5894e0ccb114d36b57fc18ddbfa83da Mon Sep 17 00:00:00 2001 From: Volodymyr Makukha Date: Mon, 19 Apr 2021 14:24:49 +0300 Subject: [PATCH 2/2] chore: e2e tests for 500 error --- e2e/codecept.conf.js | 1 + e2e/spec/500.spec.e2e.ts | 39 ++ e2e/spec/news.spec.e2e.ts | 9 - e2e/spec/pages/common.ts | 8 + e2e/spec/pages/news.ts | 1 - e2e/typings/steps.d.ts | 5 +- ilc/server/errorHandler/ErrorHandler.spec.js | 2 + registry/server/seeds/00_cleanup.ts | 3 +- registry/server/seeds/02_templates.ts | 6 + registry/server/seeds/06_routerDomains.ts | 11 + .../data/templates/500ForLocalhostAsIPv4.html | 396 ++++++++++++++++++ 11 files changed, 468 insertions(+), 13 deletions(-) create mode 100644 e2e/spec/500.spec.e2e.ts create mode 100644 e2e/spec/pages/common.ts create mode 100644 registry/server/seeds/06_routerDomains.ts create mode 100644 registry/server/seeds/data/templates/500ForLocalhostAsIPv4.html diff --git a/e2e/codecept.conf.js b/e2e/codecept.conf.js index 8df4195b..2f4075ee 100644 --- a/e2e/codecept.conf.js +++ b/e2e/codecept.conf.js @@ -42,6 +42,7 @@ exports.config = { newsPage: path.join(__dirname, 'spec', 'pages', 'news.ts'), planetsPage: path.join(__dirname, 'spec', 'pages', 'planets.ts'), hooksPage: path.join(__dirname, 'spec', 'pages', 'hooks.ts'), + common: path.join(__dirname, 'spec', 'pages', 'common.ts'), }, tests: './spec/**/*.spec.e2e.ts', name: 'ilc', diff --git a/e2e/spec/500.spec.e2e.ts b/e2e/spec/500.spec.e2e.ts new file mode 100644 index 00000000..d8b1f072 --- /dev/null +++ b/e2e/spec/500.spec.e2e.ts @@ -0,0 +1,39 @@ +Feature('500 error handling'); + +//region 500 page +Scenario('Renders 500 page for domain "localhost:8233" (default)', (I, common: common) => { + I.amOnPage(common.url.urlInternalServerError); + I.seeInSource(common.textError500); + I.seeInSource(common.textErrorId); + I.dontSeeInSource(common.textError500ForLocalhostAsIPv4); + I.seeInCurrentUrl(common.url.urlInternalServerError); +}); +Scenario('should open 500 error page when an error happens for domain "localhost:8233" (default)', async (I, newsPage: newsPage, common: common) => { + I.amOnPage(newsPage.url.main); + I.waitInUrl(newsPage.url.main, 10); + I.waitForElement(newsPage.generateError, 10); + I.click(newsPage.generateError); + I.seeInSource(common.textError500); + I.seeInSource(common.textErrorId); + I.dontSeeInSource(common.textError500ForLocalhostAsIPv4); + I.seeInCurrentUrl(newsPage.url.main); +}); + +Scenario('Renders 500 page for domain "127.0.0.1:8233"', (I, common: common) => { + I.amOnPage(common.url.localhostAsIPv4 + common.url.urlInternalServerError); + I.seeInSource(common.textError500ForLocalhostAsIPv4); + I.seeInSource(common.textErrorId); + I.dontSeeInSource(common.textError500); + I.seeInCurrentUrl(common.url.localhostAsIPv4 + common.url.urlInternalServerError); +}); +Scenario('should open 500 error page when an error happens for domain "127.0.0.1:8233"', async (I, newsPage: newsPage, common: common) => { + I.amOnPage(common.url.localhostAsIPv4 + newsPage.url.main); + I.waitInUrl(newsPage.url.main, 10); + I.waitForElement(newsPage.generateError, 10); + I.click(newsPage.generateError); + I.seeInSource(common.textError500ForLocalhostAsIPv4); + I.seeInSource(common.textErrorId); + I.dontSeeInSource(common.textError500); + I.seeInCurrentUrl(common.url.localhostAsIPv4 + newsPage.url.main); +}); +//endregion 500 page diff --git a/e2e/spec/news.spec.e2e.ts b/e2e/spec/news.spec.e2e.ts index c6a4a2b4..5c3a4727 100644 --- a/e2e/spec/news.spec.e2e.ts +++ b/e2e/spec/news.spec.e2e.ts @@ -36,12 +36,3 @@ Scenario('should open an article page from a direct link', async (I, newsPage: n I.seeInCurrentUrl(lastNewsSourceLinkHref); I.closeOtherTabs(); }); - -Scenario('should open 500 error page when an error happens', async (I, newsPage: newsPage) => { - I.amOnPage(newsPage.url.main); - I.waitInUrl(newsPage.url.main, 10); - I.waitForElement(newsPage.generateError, 10); - I.click(newsPage.generateError); - I.waitForElement(newsPage.errorId); - I.seeInCurrentUrl(newsPage.url.main); -}); diff --git a/e2e/spec/pages/common.ts b/e2e/spec/pages/common.ts new file mode 100644 index 00000000..42990496 --- /dev/null +++ b/e2e/spec/pages/common.ts @@ -0,0 +1,8 @@ +export const url = { + urlInternalServerError: '/_ilc/500', + localhostAsIPv4: 'http://127.0.0.1:8233', +}; + +export const textError500 = '

ERROR 500

'; +export const textError500ForLocalhostAsIPv4 = '

500 Internal Server Error on 127.0.0.1

'; +export const textErrorId = 'Error ID:'; diff --git a/e2e/spec/pages/news.ts b/e2e/spec/pages/news.ts index 7c161028..6f252bd9 100644 --- a/e2e/spec/pages/news.ts +++ b/e2e/spec/pages/news.ts @@ -14,7 +14,6 @@ export const newsView = 'body > div#body > div.single-spa-container.news-app > d export const newsSources = `${newsView} > div.sources > div.container > ol > li.source`; export const bannerHeadline = `${newsView} > div.banner > h1`; export const generateError = `${newsView} > div.banner > a`; -export const errorId = `body > div#error > p:nth-child(3)`; export const lastNewsSource = `${newsSources}:last-child`; export const lastNewsSourceLink = `${lastNewsSource} > p.action > a`; export const newsSourceArticles = `${newsView} > div.container > div.articles > ol > li.article`; diff --git a/e2e/typings/steps.d.ts b/e2e/typings/steps.d.ts index 5abe9cf2..78e42249 100644 --- a/e2e/typings/steps.d.ts +++ b/e2e/typings/steps.d.ts @@ -3,11 +3,12 @@ type peoplePage = typeof import('../spec/pages/people'); type newsPage = typeof import('../spec/pages/news'); type planetsPage = typeof import('../spec/pages/planets'); type hooksPage = typeof import('../spec/pages/hooks'); +type common = typeof import('../spec/pages/common'); type MockRequestHelper = import('@codeceptjs/mock-request'); declare namespace CodeceptJS { - interface SupportObject { I: CodeceptJS.I, peoplePage: peoplePage, newsPage: newsPage, planetsPage: planetsPage, hooksPage: hooksPage } - interface CallbackOrder { [0]: CodeceptJS.I; [1]: peoplePage; [2]: newsPage; [3]: planetsPage; [4]: hooksPage } + interface SupportObject { I: CodeceptJS.I, peoplePage: peoplePage, newsPage: newsPage, planetsPage: planetsPage, hooksPage: hooksPage, common: common } + interface CallbackOrder { [0]: CodeceptJS.I; [1]: peoplePage; [2]: newsPage; [3]: planetsPage; [4]: hooksPage; [5]: common } interface Methods extends CodeceptJS.Puppeteer, MockRequestHelper {} interface I extends WithTranslation {} namespace Translation { diff --git a/ilc/server/errorHandler/ErrorHandler.spec.js b/ilc/server/errorHandler/ErrorHandler.spec.js index 5ec0b3e7..1a1c0410 100644 --- a/ilc/server/errorHandler/ErrorHandler.spec.js +++ b/ilc/server/errorHandler/ErrorHandler.spec.js @@ -29,6 +29,7 @@ describe('ErrorHandler', () => { }); it('should show 500 error page with an error id', async () => { + nock(config.get('registry').address).get('/api/v1/router_domains').reply(200, []); nock(config.get('registry').address).get(`/api/v1/template/500/rendered`).reply(200, { content: '' + @@ -59,6 +60,7 @@ describe('ErrorHandler', () => { }); it('should send an error message when showing 500 error page throws an error', async () => { + nock(config.get('registry').address).get('/api/v1/router_domains').reply(200, []); const replyingError = new Error('Something awful happened.'); nock(config.get('registry').address).get(`/api/v1/template/500/rendered`).replyWithError(replyingError.message); diff --git a/registry/server/seeds/00_cleanup.ts b/registry/server/seeds/00_cleanup.ts index b17668de..3d60957b 100644 --- a/registry/server/seeds/00_cleanup.ts +++ b/registry/server/seeds/00_cleanup.ts @@ -8,8 +8,9 @@ export async function seed(knex: Knex): Promise { await knex('route_slots').transacting(trx).truncate(); await knex('routes').transacting(trx).truncate(); await knex('apps').transacting(trx).truncate(); - await knex('templates').transacting(trx).truncate(); await knex('shared_props').transacting(trx).truncate(); + await knex('router_domains').transacting(trx).truncate(); + await knex('templates').transacting(trx).truncate(); } finally { isMySQL(knex) && await knex.schema.raw('SET FOREIGN_KEY_CHECKS = 1;').transacting(trx); } diff --git a/registry/server/seeds/02_templates.ts b/registry/server/seeds/02_templates.ts index ff66d94e..b36b84d2 100644 --- a/registry/server/seeds/02_templates.ts +++ b/registry/server/seeds/02_templates.ts @@ -17,5 +17,11 @@ export async function seed(knex: Knex): Promise { encoding: 'utf8', }) }, + { + name: '500ForLocalhostAsIPv4', + content: fs.readFileSync(path.join(__dirname, './data/templates/500ForLocalhostAsIPv4.html'), { + encoding: 'utf8', + }) + }, ]); } diff --git a/registry/server/seeds/06_routerDomains.ts b/registry/server/seeds/06_routerDomains.ts new file mode 100644 index 00000000..68504dce --- /dev/null +++ b/registry/server/seeds/06_routerDomains.ts @@ -0,0 +1,11 @@ +import * as Knex from 'knex'; + +export async function seed(knex: Knex): Promise { + return knex("router_domains").insert([ + { + id: 1, + domainName: '127.0.0.1:8233', + template500: '500ForLocalhostAsIPv4', + }, + ]); +} diff --git a/registry/server/seeds/data/templates/500ForLocalhostAsIPv4.html b/registry/server/seeds/data/templates/500ForLocalhostAsIPv4.html new file mode 100644 index 00000000..9f1152d1 --- /dev/null +++ b/registry/server/seeds/data/templates/500ForLocalhostAsIPv4.html @@ -0,0 +1,396 @@ + + + + + + + + + + + +
+
+

500 Internal Server Error on 127.0.0.1

+

%ERRORID%

+

Things are a little unstable here

+

I suggest come back later

+
+ + + \ No newline at end of file