Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to refresh custom 404 page #1979

Open
Mis1eader-dev opened this issue Mar 20, 2024 · 11 comments
Open

Unable to refresh custom 404 page #1979

Mis1eader-dev opened this issue Mar 20, 2024 · 11 comments
Labels
enhancement New feature or request

Comments

@Mis1eader-dev
Copy link
Member

Mis1eader-dev commented Mar 20, 2024

Describe the bug
In a real-time system, the 404 page may get updated, and since the custom 404 page is cached in Drogon, we have to call drogon::app().setCustom404Page to refresh its content, however, this doesn't happen.
For SPAs this is a significant issue, as the only way to refresh its content is through restarting the server.

To Reproduce
Steps to reproduce the behavior:

  1. Create a 404 page
  2. Call drogon::app().setCustom404Page and point it to the 404 file
  3. Open the browser and go somewhere random, note the current page
  4. In the backend edit the 404 page contents and save the file
  5. Refresh the browser, you'll still see the old 404 page
  6. Now create an API endpoint that calls drogon::app().setCustom404Page and returns 200 OK
  7. Call the API
  8. Still shows the old 404 page

Expected behavior
Either the 404 shouldn't be cached, or we should be allowed to refresh it.

Desktop (please complete the following information):

  • OS: Ubuntu
  • Browser: Google Chrome
@hwc0919
Copy link
Member

hwc0919 commented Mar 23, 2024

I think we don't need to pursue extreme performance in these cases. An atomic shared pointer is enough. @an-tao

const HttpResponsePtr &HttpAppFrameworkImpl::getCustom404Page()
{
if (!custom404_)
{
return custom404_;
}
auto loop = trantor::EventLoop::getEventLoopOfCurrentThread();
if (loop && loop->index() < app().getThreadNum())
{
// If the current thread is an IO thread
static IOThreadStorage<HttpResponsePtr> thread404Pages;
static std::once_flag once;
std::call_once(once, [this] {
thread404Pages.init(
[this](HttpResponsePtr &resp, size_t /*index*/) {
resp = std::make_shared<HttpResponseImpl>(
*static_cast<HttpResponseImpl *>(custom404_.get()));
});
});
return thread404Pages.getThreadData();
}
else
{
return custom404_;
}
}

@hwc0919
Copy link
Member

hwc0919 commented Mar 25, 2024

You could use setCustomErrorHandler() before we handle this problem.

The custom handler will be invoked every time, no internal caching.

PS: To make it into use, do not call setCustom404Page().

@hwc0919 hwc0919 added the enhancement New feature or request label Mar 25, 2024
@Mis1eader-dev
Copy link
Member Author

Performance-wise will newFileResponse calls in the custom error handler be cached?
If every request hitting the error handler ends up making an OS file request repeatedly then that will degrade performance quite a lot for SPAs

Say the custom error handler is set to always

return HttpResponse::newFileResponse(drogon::app().getDocumentRoot() + "/index.html");

And we have 10000 active requests every second to /some/page, does the first request do a file read and the rest will be cached until the file Date Modified changes?

@an-tao
Copy link
Member

an-tao commented Mar 27, 2024

Performance-wise will newFileResponse calls in the custom error handler be cached? If every request hitting the error handler ends up making an OS file request repeatedly then that will degrade performance quite a lot for SPAs

Say the custom error handler is set to always

return HttpResponse::newFileResponse(drogon::app().getDocumentRoot() + "/index.html");

And we have 10000 active requests every second to /some/page, does the first request do a file read and the rest will be cached until the file Date Modified changes?

You could use the setExpiredTime mehtod of HttpResponse to achieve that.

@Mis1eader-dev
Copy link
Member Author

Thank you @an-tao and @hwc0919, using a combination of resp->setExpiredTime(drogon::app().staticFilesCacheTime()) and drogon::app().setCustomErrorHandler(...) seems to achieve the desired result

I don't know whether this issue can be closed, it needs proper documentation to let users know drogon::app().setCustom404Page(...) is called only once, and will stay cached until the application exits

@Mis1eader-dev
Copy link
Member Author

This does not seem to be the way to do this:

static void postConfigure()
{
	/// vue.js integration (SPA)
	auto resp = HttpResponse::newFileResponse(
		drogon::app().getDocumentRoot() + "/index.html",
		"",
		drogon::CT_TEXT_HTML,
		"text/html"
	);
	resp->setExpiredTime(drogon::app().staticFilesCacheTime());
	drogon::app()
		.setCustomErrorHandler(
			[
				resp = std::move(resp)
			](HttpStatusCode, const HttpRequestPtr&)
			{
				return resp;
			}
		);
}

int main()
{
	drogon::app()
		.loadConfigFile("./config.json");

	postConfigure();

	drogon::app()
		.run();
}

The response will always be the old cached index.html page.

Will it be cached if the

auto resp = HttpResponse::newFileResponse(...

is moved into the custom error handler? I can't find any cache map lookup mechanism for file responses in the framework to safely do this without worrying about it hitting the OS file lookups every time

@an-tao
Copy link
Member

an-tao commented Mar 29, 2024

You should move the newFileResponse code to the handler and cache the old response by yourself.

@Mis1eader-dev
Copy link
Member Author

I found drogon::StaticFileRouter::sendStaticFileResponse(...) but it is not publicly exposed to be used outside of the framework, and it seems to take in a response callback, which the custom error handler doesn't have.

The only way without reimplementing the static file router would be to create individual pages with HttpSimpleControllers and have each one call drogon::StaticFileRouter::sendStaticFileResponse(...) with the index.html file.

Seems we have to use an atomic drogon::HttpResponsePtr and have the endpoint that gets notified of the page changes assign a new drogon::HttpResponsePtr to the atomic response

@Mis1eader-dev
Copy link
Member Author

Mis1eader-dev commented Mar 29, 2024

For other SPA developers, this is the snippet that may be of help to you

static std::atomic<HttpResponsePtr> indexHtml;

static void updateIndexHtml()
{
	::indexHtml = HttpResponse::newFileResponse(
		drogon::app().getDocumentRoot() + "/index.html",
		"",
		drogon::CT_TEXT_HTML,
		"text/html"
	);
}

static void postConfigure()
{
	/// vue.js integration (SPA)
	updateIndexHtml();
	drogon::app()
		.setCustomErrorHandler(
			[](HttpStatusCode, const HttpRequestPtr&)
			{
				return ::indexHtml.load();
			}
		)
		/// WebSocket notification of frontend change
		.registerHandler(
			"/api/_/dist", // <- can be anything you like
			[](const HttpRequestPtr &req,
				std::function<void (const HttpResponsePtr&)>&& callback)
			{
				if(!req->peerAddr().isLoopbackIp())
				{
					callback(HttpResponse::newNotFoundResponse(std::move(req)));
					return;
				}

				updateIndexHtml();

				callback(HttpResponse::newHttpResponse(drogon::k200OK, drogon::CT_NONE));

				// TODO: Notify the frontend clients to refresh their pages
			}
		)
		;
}

int main()
{
	drogon::app()
		.loadConfigFile("./config.json")
		;

	postConfigure();

	drogon::app()
		.run()
		;
}

You will need some client code somewhere to trigger the /api/_/dist endpoint, a simple curl will refresh the cache:

curl -d '' http://localhost:8080/api/_/dist

If you need to trigger it remotely, you can remove the check

if(!req->peerAddr().isLoopbackIp())
{
	callback(HttpResponse::newNotFoundResponse(std::move(req)));
	return;
}

However, beware of the security risk of removing that check, as anybody could hit that API endpoint and consume your server's resources from constant reloading of the index.html file

@hwc0919
Copy link
Member

hwc0919 commented Mar 29, 2024

You can use an timer to check the md5 change or edit time of the file.

@Mis1eader-dev
Copy link
Member Author

Thank you for the suggestion, in my use case there is a custom CI/CD app made with Drogon that gets triggered by GitHub webhooks, which performs a git pull on the document_root directory and makes a localhost request to the actual server to notify its clients to refresh their pages.

So performance-wise this atomic variable is enough for this application.

If other developers needed automatic refresh, they have to go the MD5 route

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants