I recently started developing a messaging service aggregator, and I thought it would be really funny to port it's webapp to as many browsers as possible. This post details all the cursed knowledge about old browsers that I now have.
Basic WebApp
The main part of the webapp is the chat interface, where you can send and receive messages. At this point it was pretty simple: a list of messages generated with jinja2, and a form to send more. This should render absolutely fine in all browsers.
NetScape Navigator 3.0 (Windows 3.11)

Setting up Windows 3.11 (for Workgroups) with networking was quite a pain (this video helped), but after I got it working, NetScape rendered the chat interface perfectly.
A problem arose when I added options (for example, the per-page message limit). I decided to pass them through the url query string (the part after ?), as cookies is a thing that many old browsers don't support. I made it so that the template always appends the current url query to the links it generates, so that the options are preserved across pages.
Unfortunately Netscape is too old to know what a query string is, and just treats it as a part of the path. This causes problems for relative links: if a modern browser encouters a link to ?a=1, while on /chat, it will navigate to /chat?a=1, but Netscape will go to /?a=1 instead. I solved this by making all urls absolute in the template.
Chrome 1.0 (Windows XP)

(I was using Chrome 1.0.154.59)
It just worked, so I decided to make things more interesting: I added javascript. The purpose of the script is to watch for incoming messages and adding them to the page as they come. It also allows sending messages without refreshing the page.
I used the XHttpRequest api to make the requests. It's quite an ald standard, making it great for portability, but it also allows you to stream the response live from the server, so I don't have to constatly poll it for new messages.
Chrome 1.0 does support this api, but it doesn't support JSON. I though for a moment about making a json parser, but decided against it, as the standard is really complex and I don't want to spend much time on this, or copy a giant parser someone else wrote. Instead I decided to just make my own serialization format, specifically for the purpose of easy deserialization.
The format is named SOR (Simple Object Representation) and it is as follows:
A SOR object is an ASCII string. The first letter of it represents it's type
The type
n(null) should be followed by;The type
b(boolean) should be followed byt(true) orf(false) and then;The type
i(integer) should be followed by an integer, and then;The type
s(string) should be followed by any chraracters (except a newline or;). If a\is encountered, it should be followed by a SOR integer, representing the ascii (or unicode) code of a characterThe type
a(array) should be followed by an arbitrary amount of SOR objects, and then a;The type
o(object) should be followed by an arbitrary amount of pairs of SOR objects, the first of the pair should be a string, representing the key, and the second -- any object, representing the value, and then a;
Chrome didn't seem to properly give me partial data, so I have to use a longpoll instead of streaming. I decided that instead of hard-coding a list of supported browsers, it would be better to make it automatically detect if streaming was supported or not. If after a second it doesn't receive a special marker that the server sends at the beginning of the connection, then it aborts the request and starts a new one using longpoll.
Now I've ran into another problem. The entire tab would lock up while the request was in progress and would update once when some data arrived, as Chrome back then did everything in a single thread (I assume). I fixed this by making the server send null 10 times per second, which is fast enough to make the ui somewhat responsive. This also magically fixed the streaming issue I had.
Internet Explorer 6.0 (Windows XP)

IE6 (unlike, curiously, previous versions of IE) has javascript disabled by default. It also doesn't have a XHttpRequest, but it does have a ActiveXObject("Microsoft.XMLHTTP"), which is basically the same, but microsoft branded. The main caveat with it is that you have to explicitly allow it in the settings, otherwise it throws a cryptic "Automation server can't create object" error. Though the Error object on EI6 allows me to read it's message, so I can detect this error and warn the user.
It also had a lot of missing features, which are pretty easy to polyfill, or just ignore:
You can't iterate over a string
You can't set the
typeof a buttonYou can't use string literals as keys in an object literal
You can't use
console.log,Event.stopPropagationandpreventDefault,document.addEventListeneranddocument.getElementsByClassNameYou can't use
Element.textContent, you have to useinnerTextinstead
Internet Explorer 3.0 (Windows 95)

This wasn't a successful port. IE3 does support javascript (or JScript, as it was called in IE back then), but it only supports DOM level 0, meaning that only forms, inputs and anchors can be accessed. This basically stops all attempts to port chatdemon to it dead in the water. The best I can do is just disable everything and not throw an error.
Internet Explorer 5.0 (Windows 98)

It's basically the same as IE6, except all the required features (Javascript and XMLHTTP) are enabled by default, and there are a few new quirks:
encodeURIComponent is not available, requiring me to implement it from scratch.
Interacting with an undefined value in any way, even just getting the type of it or checking if it's equal to itself, throws an error. This means that the only way (at least that I know of) to check if a value is undefined is with try ... catch ... (which, thankfully, is available here)
And a really cursed one -- and I'm not entirely sure I'm describing it correctly -- if you try to pass an anomymous function to setTimeout inside an XHR callback, IE will crash with a page fault, and I have no idea why, but the workaround here is to only use named functions in setTimeout.
WorldWideWeb 0.15 (NeXTSTEP 3.0)

WorldWideWeb is the very first web browser, so it really stuggles to properly render html. There is no javascript to speak of, in fact, the browser is too old to know what a <script> tag is, so it just displays the text inside it. (Though you can just disable javascript in the chatdemon options, and then it won't appear). It is so old, that if you don't add a / at the end of the url, then the packet it sends doesn't even get recognized by Flask as a valid HTTP request.
The very first problem is that the chats in the chat list are all on the same line, as they are just links. Wrapping them in <p>s didn't seem to do anything, this browser doesn't know what a <br> is, but wrapping them in <h6>s did work, so that's what I'm using.
Next, a link problem. Jinja2 automatically html-encodes the contents of variables, so when a = "a&b", and <a href="{{a}}"> turns into <a href="a&b">. All the browsers I've tried so far were smart enough to decode this, but WWW isn't. I solved this by wrapping all urls in Markup, preventing this escaping. This does mean that a maliciously crafted url might generate any html it likes, but this is a single-user app, so it doesn't matter.
Now onto the actual chat screen. Currently it has a bunch of junk on it (which seems to be coming from the class= attributes and javascript):

To figure out why this text appears, I studied WWW's source code, and here's what I managed to figure out:
The problematic parsing occurs when it tries to parse <div class="message">. In ParseHTML.h. The parser is a simple state machine, and after reading <d (start of a div tag) it does the following:
case S_tag_d:
switch(c) {
case 'L':
case 'l':
/* Start Definition list <DL> */
(void) check("L> <"); /* Ignore first DT */
c = NEXT_CHAR;
if (c=='/') {
check("/DL>");
} else {
(void) check("DT>");
start_style(&Glossary);
}
SETSTATE(S_text);
case 'T':
case 't':
/* Definition term <DT> */
output_paragraph();
SETSTATE(S_junk_tag);
case 'D':
case 'd':
/* Definition definition <DD> */
OUTPUT('\t');
SETSTATE(S_junk_tag);
} /*switch c */
break;
It's too old to know what a div is, so after the d it expects either l, t or another d, but instead it gets an i. In this case it continues to the break; and doesn't change the state. This happens until it encounters one of the expected characters, which happens inside "class".
After that it assumes that it has hit a \<dl> tag, and runs check("L> <"), which does nothing, as the expected characters aren't present. It then reads the next character, sets the style and goes into state S_text, meaning the final output is:
ss="message">
The same this actually happens on the main page, where it gobbles up the first url in the header.
The best option here would be to skip the unknown tag entirely, unfortunately most of the useful states in this case: S_junk_line and S_junk_script (both of which ignore text until the end of the line), cannot be entered, but there is one left: S_junk_tag (it ignores text until >). We can get to it from S_tag_d by encountering t or d, so if I change the tag to <div t="1" class="message">. It will hit the t, and the ignore the rest of the tag, leading to the site you saw at the beginning of this section.
I can also use the same trick to hide the script text even when javascript is enabled, I just have to not use > inside the script, and because only a small part of the script is directly in the document, that's quite easy.
So, WWW can now technically render my webapp. It's not pretty, most things don't work (like formatting or sending messages), but it opens.
And that's as far as you can go back in time before a "web" app stops making sense.
highghlow