Skip to main content

How It Works

Summary

Here's a quick high-level summary of how fallback() works. A more in-depth explanation is included further below on this page, under Full Request/Response Lifecycle.

  1. The calling contract or app serializes an HTTP request into bytes. For example:

    // Convert HTTP request string into bytes in Solidity
    bytes memory requestBytes = bytes("GET /github HTTP/1.1");
    // Convert HTTP request string into bytes in Node.js
    const requestBytes = Buffer.from("GET /github HTTP/1.1").toString("hex");
  2. The calling contract or app calls the fallback() server contract with the serialized HTTP request bytes:

    // Execute low-level call on the fallback() server contract
    (bool success, bytes memory responseBytes) = myServer.call(requestBytes);

    // The fallback() contract should handle arbitrary input,
    // including malformed requests (which should return a 400
    // response) so `success` will generally not be `false` (if
    // it is `false`, it's a bug!).
  3. The calling contract or app can deserialize the response bytes into a valid HTTP response.

    // Convert bytes into string to read response
    string memory responseString = string(responseBytes);

    // `responseString` will be "HTTP/1.1 200 OK..."
    // (or "HTTP/1.1 302 Found..." if this is a redirect,
    // etc.)

Thanks @gruns for suggesting and outlining this summary!

Example Transaction

Here's a screenshot of the input and output data of an example transaction served by send-server.js.

This example request is run against an instance of the TodoServer example fallback() app, which is deployed at 0x919F31dAC93eBf9fFd15a54acd13082f34fDd6D3 on the Goerli Optimism testnet.

Input Data

input data

Output Data

output data

Full Request/Response Lifecycle

For a more in-depth look at how fallback() works, we can trace through all the contract calls that occur when you send a request to the MyServer created in the Quick Start. An abridged version of the MyServer and MyApp contracts is reproduced below:

MyApp.sol
contract MyApp is WebApp {
constructor() {
routes[HttpConstants.Method.GET]["/"] = "getIndex";
routes[HttpConstants.Method.GET]["/github"] = "getGithub";
}

function getIndex(HttpMessages.Request calldata request) external pure override returns (HttpMessages.Response memory) {
string memory htmlString = H.html5(
H.body(
StringConcat.concat(
H.h1("fallback() web framework"),
H.p(H.i("a solidity web framework"))
)
)
);
return html(htmlString);
}

function getGithub() external pure returns (HttpMessages.Response memory) {
return redirect(302, "https://github.com/nathanhleung/fallback");
}
}

contract MyServer is DefaultServer {
constructor() DefaultServer(new MyApp()) {
app.setDebug(true);
}
}

1. MyServer

First, we send the hex-encoded bytes of an HTTP request to MyServer:

Example Request in JavaScript
const request = "GET /github HTTP/1.1";
const result = await web3.eth.call({
to: MYSERVER_CONTRACT_ADDRESS,
data: Buffer.from(request).toString("hex");
});

MyServer has no logic of its own besides its constructor, so when it receives a request, it's actually handled by DefaultServer.

MyServer.sol
contract MyServer is DefaultServer

2. DefaultServer

HttpServer.sol
contract DefaultServer is HttpServer

DefaultServer also has no signficant logic of its own — it simply extends the HttpServer contract and automatically sets a few reasonable request parsing defaults (e.g. it sets maximumRequestHeaders to 4000 and maxPathLength to 4000).

These defaults are generally based on Apache's defaults.

So we follow the inheritance chain up to HttpServer.

3. HttpServer

HttpServer.sol
contract HttpServer is HttpProxy

HttpServer, in turn, extends HttpProxy. HttpServer is essentially a public API for HttpProxy since HttpProxy has no constructor.

4. HttpProxy Part 1

HttpProxy is a contract whose only external or public function is a fallback function (hence, the name of this project).

When HttpProxy receives a transaction with data, since there are no functions on the contract, the fallback function will be called instead.

In the fallback function, HttpProxy uses the HttpMessages contract to parse the calldata as if it were an HTTP request and passes the parsed request to HttpHandler.

HttpProxy.sol
// Simplified version of `HttpProxy`
contract HttpProxy {
HttpHandler internal handler;
HttpMessages internal messages;

fallback() external {
bytes memory requestBytes = msg.data;
// Parse request
HttpMessages.Request memory request =
messages.parseRequest(requestBytes);
// Call route handler
HttpMessages.Response memory response =
handler.handleRoute(request);
// ...
}

In our case, our original request

const request = "GET /github HTTP/1.1";

would be parsed into something that look like this:

HttpMessages.Request {
method: HttpConstants.Method.GET,
path: "/github",
headers: [],
contentLength: 0,
content: "",
raw: "0x..."
}

5. HttpHandler

HttpHandler's handleRoute function looks at the parsed HTTP method and path and checks to see if there is a corresponding route set in the routes mapping in WebApp (in our case, MyApp).

MyApp.sol
contract MyApp is WebApp {
constructor() {
routes[HttpConstants.Method.GET]["/"] = "getIndex";
routes[HttpConstants.Method.GET]["/github"] = "getGithub";
}
}

If there is a route configured, HttpHandler executes a low-level call to the configured function on the instance of WebApp and forwards the response back to HttpServer.

In our case, HttpHandler will map our GET /github request to the getGithub function on MyApp and call it.

MyApp.sol
function getGithub() external pure returns (HttpMessages.Response memory) {
return redirect(302, "https://github.com/nathanhleung/fallback");
}

The return value from getGithub will be passed to handleRoute. It will be an HttpMessages.Response struct that looks like this:

HttpMessages.Response {
statusCode: 302,
headers: ["Location: https://github.com/nathanhleung/fallback"],
content: ""
}

6. HttpProxy Part 2

handleRoute will return the response back to HttpProxy.

Finally, HttpMessages is used again to serialize the response into bytes and return it to the caller.

HttpProxy.sol
// Simplified version of `HttpProxy`
contract HttpProxy {
HttpHandler internal handler;
HttpMessages internal messages;

fallback() external {
bytes memory requestBytes = msg.data;

// Parse request
HttpMessages.Request memory request =
messages.parseRequest(requestBytes);

// Call route handler
HttpMessages.Response memory response =
handler.handleRoute(request);

// Serialize `Response` struct into HTTP response bytes
bytes memory responseBytes = messages.buildResponse(response);

// Emit event so callers can access response data in the
// transaction receipt, too.
emit Response(responseBytes);

return responseBytes;

// (We're using `return` above for simplicity, but technically, you
// can't `return` from`fallback` — the above is actually implemented in
// inline assembly).
}

7. Error Handling

The code samples above are slightly simplified and don't show all the error handling code.

If there is no route configured, HttpHandler's handleRoute will return a 404 response.

If an error occurs during request handling, a 400 (Bad Request) or 500 (Internal Server Error) error will be returned depending on where exactly the error occurred (generally, 400 if it's in HttpProxy and 500 if it's in HttpHandler).

If debug mode is enabled on WebApp, the error pages will show the full request and response data. In debug mode, example error pages can be accessed at the paths /__not_found (404), /__bad_request (400), and /__error (500).