changeset 1607:fa066aaa068c

nginx caching
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 30 Apr 2021 20:23:28 -0600
parents 7c7f28c724e8
children f7e3adae4907
files src/goodjava/util/CacheMap.java src/goodjava/util/CaseInsensitiveMap.java src/goodjava/webserver/Connection.java src/goodjava/webserver/Request.java src/goodjava/webserver/RequestParser.java src/goodjava/webserver/Response.java src/goodjava/webserver/examples/Cookies.java src/goodjava/webserver/examples/Example.java src/goodjava/webserver/examples/Headers.java src/goodjava/webserver/examples/Params.java src/goodjava/webserver/handlers/ContentTypeHandler.java src/goodjava/webserver/handlers/DirHandler.java src/goodjava/webserver/handlers/FileHandler.java src/goodjava/webserver/handlers/HeadersHandler.java src/goodjava/webserver/handlers/SafeHandler.java src/luan/LuanTable.java src/luan/host/WebHandler.java src/luan/modules/BasicLuan.java src/luan/modules/Table.luan src/luan/modules/http/Http.luan src/luan/modules/http/Server.luan src/luan/modules/http/tools/Run.luan src/luan/modules/parsers/LuanToString.java
diffstat 23 files changed, 330 insertions(+), 112 deletions(-) [+]
line wrap: on
line diff
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/util/CacheMap.java
--- a/src/goodjava/util/CacheMap.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/util/CacheMap.java	Fri Apr 30 20:23:28 2021 -0600
@@ -34,36 +34,36 @@
 		}
 	}
 
-	public int size() {
+	@Override public int size() {
 		return cache.size();
 	}
 
-	public boolean isEmpty() {
+	@Override public boolean isEmpty() {
 		return cache.isEmpty();
 	}
 
-	public boolean containsKey(Object key) {
+	@Override public boolean containsKey(Object key) {
 		return cache.containsKey(key);
 	}
 
-	public V get(Object key) {
+	@Override public V get(Object key) {
 		MyReference<K,V> ref = cache.get(key);
 		return ref==null ? null : ref.get();
 	}
 
-	public V put(K key,V value) {
+	@Override public V put(K key,V value) {
 		sweep();
 		MyReference<K,V> ref = cache.put( key, newReference(key,value,queue) );
 		return ref==null ? null : ref.get();
 	}
 
-	public V remove(Object key) {
+	@Override public V remove(Object key) {
 		sweep();
 		MyReference<K,V> ref = cache.remove(key);
 		return ref==null ? null : ref.get();
 	}
 
-	public void clear() {
+	@Override public void clear() {
 		sweep();
 		cache.clear();
 	}
@@ -75,43 +75,39 @@
 		return map;
 	}
 */
-	public Set<K> keySet() {
+	@Override public Set<K> keySet() {
 		return cache.keySet();
 	}
 
-	public Set<Map.Entry<K,V>> entrySet() {
+	@Override public Set<Map.Entry<K,V>> entrySet() {
 		return new MySet();
 	}
 
 
 	private class MySet extends AbstractSet<Map.Entry<K,V>> {
 
-		public int size() {
+		@Override public int size() {
 			return CacheMap.this.size();
 		}
 
-		public Iterator<Map.Entry<K,V>> iterator() {
-			return new MyIterator(cache.entrySet().iterator());
+		@Override public Iterator<Map.Entry<K,V>> iterator() {
+			return new MyIterator();
 		}
 
 	}
 
 	private class MyIterator implements Iterator<Map.Entry<K,V>> {
-		Iterator<Map.Entry<K,MyReference<K,V>>> iter;
+		final Iterator<Map.Entry<K,MyReference<K,V>>> iter = cache.entrySet().iterator();
 
-		MyIterator(Iterator<Map.Entry<K,MyReference<K,V>>> iter) {
-			this.iter = iter;
-		}
-
-		public boolean hasNext() {
+		@Override public boolean hasNext() {
 			return iter.hasNext();
 		}
 
-		public void remove() {
+		@Override public void remove() {
 			iter.remove();
 		}
 
-		public Map.Entry<K,V> next() {
+		@Override public Map.Entry<K,V> next() {
 			return new MyEntry( iter.next() );
 		}
 	}
@@ -123,21 +119,21 @@
 			this.entry = entry;
 		}
 
-		public K getKey() {
+		@Override public K getKey() {
 			return entry.getKey();
 		}
 
-		public V getValue() {
+		@Override public V getValue() {
 			MyReference<K,V> ref = entry.getValue();
 			return ref.get();
 		}
 
-		public V setValue(V value) {
+		@Override public V setValue(V value) {
 			MyReference<K,V> ref = entry.setValue( newReference(getKey(),value,queue) );
 			return ref.get();
 		}
 
-		public boolean equals(Object o) {
+		@Override public boolean equals(Object o) {
 			if( o==null || !(o instanceof CacheMap.MyEntry) )
 				return false;
 			@SuppressWarnings("unchecked")
@@ -145,7 +141,7 @@
 			return entry.equals(m.entry);
 		}
 
-		public int hashCode() {
+		@Override public int hashCode() {
 			K key = getKey();
 			V value = getValue();
 			return (key==null ? 0 : key.hashCode()) ^
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/util/CaseInsensitiveMap.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/util/CaseInsensitiveMap.java	Fri Apr 30 20:23:28 2021 -0600
@@ -0,0 +1,99 @@
+package goodjava.util;
+
+import java.util.Map;
+import java.util.AbstractMap;
+import java.util.Set;
+import java.util.AbstractSet;
+import java.util.Iterator;
+
+
+public final class CaseInsensitiveMap<V> extends AbstractMap<String,V> {
+
+	public static final class Value<V> {
+		private final String s;
+		private final V v;
+
+		private Value(String s,V v) {
+			this.s = s;
+			this.v = v;
+		}
+	}
+
+	private final Map<String,Value<V>> map;
+
+	public CaseInsensitiveMap(Map<String,Value<V>> map) {
+		this.map = map;
+	}
+
+	@Override public int size() {
+		return map.size();
+	}
+
+	@Override public boolean isEmpty() {
+		return map.isEmpty();
+	}
+
+	@Override public boolean containsKey(Object key) {
+		if( !(key instanceof String) )
+			return false;
+		String s = (String)key;
+		return map.containsKey(s.toLowerCase());
+	}
+
+	@Override public V get(Object key) {
+		if( !(key instanceof String) )
+			return null;
+		String s = (String)key;
+		Value<V> val = map.get(s.toLowerCase());
+		return val==null ? null : val.v;
+	}
+
+	@Override public V put(String key,V value) {
+		Value<V> val = map.put( key.toLowerCase(), new Value<V>(key,value) );
+		return val==null ? null : val.v;
+	}
+
+	@Override public V remove(Object key) {
+		if( !(key instanceof String) )
+			return null;
+		String s = (String)key;
+		Value<V> val = map.remove(s.toLowerCase());
+		return val==null ? null : val.v;
+	}
+
+	@Override public void clear() {
+		map.clear();
+	}
+
+	@Override public Set<Map.Entry<String,V>> entrySet() {
+		return new MySet();
+	}
+
+	private class MySet extends AbstractSet<Map.Entry<String,V>> {
+
+		@Override public int size() {
+			return CaseInsensitiveMap.this.size();
+		}
+
+		@Override public Iterator<Map.Entry<String,V>> iterator() {
+			return new MyIterator();
+		}
+	}
+
+	private class MyIterator implements Iterator<Map.Entry<String,V>> {
+		final Iterator<Map.Entry<String,Value<V>>> iter = map.entrySet().iterator();
+
+		@Override public Map.Entry<String,V> next() {
+			Value<V> val = iter.next().getValue();
+			return new AbstractMap.SimpleImmutableEntry<String,V>( val.s, val.v );
+		}
+
+		@Override public boolean hasNext() {
+			return iter.hasNext();
+		}
+
+		@Override public void remove() {
+			iter.remove();
+		}
+	}
+}
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/Connection.java
--- a/src/goodjava/webserver/Connection.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/Connection.java	Fri Apr 30 20:23:28 2021 -0600
@@ -114,8 +114,8 @@
 					msg = "invalid content for content-type " + contentType + "\n" + msg;
 				response = Response.errorResponse(Status.BAD_REQUEST,msg);
 			}
-			response.headers.put("connection","close");
-			response.headers.put("content-length",Long.toString(response.body.length));
+			response.headers.put("Connection","close");
+			response.headers.put("Content-Length",Long.toString(response.body.length));
 			byte[] header = response.toHeaderString().getBytes();
 	
 			OutputStream out = socket.getOutputStream();
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/Request.java
--- a/src/goodjava/webserver/Request.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/Request.java	Fri Apr 30 20:23:28 2021 -0600
@@ -3,6 +3,7 @@
 import java.util.Map;
 import java.util.LinkedHashMap;
 import java.util.Collections;
+import goodjava.util.CaseInsensitiveMap;
 
 
 public class Request {
@@ -13,7 +14,7 @@
 	public volatile String path;
 	public volatile String protocol;  // only HTTP/1.1 is accepted
 	public volatile String scheme;
-	public final Map<String,Object> headers = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
+	public final Map<String,Object> headers = Collections.synchronizedMap(new CaseInsensitiveMap<Object>(new LinkedHashMap<String,CaseInsensitiveMap.Value<Object>>()));
 	public final Map<String,Object> parameters = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
 	public final Map<String,String> cookies = Collections.synchronizedMap(new LinkedHashMap<String,String>());
 	public volatile byte[] body;
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/RequestParser.java
--- a/src/goodjava/webserver/RequestParser.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/RequestParser.java	Fri Apr 30 20:23:28 2021 -0600
@@ -127,7 +127,7 @@
 		int start = parser.currentIndex();
 		require( tokenChar() );
 		while( tokenChar() );
-		return parser.textFrom(start).toLowerCase();
+		return parser.textFrom(start);
 	}
 
 	private String parseValue() throws ParseException {
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/Response.java
--- a/src/goodjava/webserver/Response.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/Response.java	Fri Apr 30 20:23:28 2021 -0600
@@ -6,14 +6,15 @@
 import java.util.LinkedHashMap;
 import java.util.Collections;
 import java.util.List;
+import goodjava.util.CaseInsensitiveMap;
 
 
 public class Response {
 	public final String protocol = "HTTP/1.1";
 	public volatile Status status = Status.OK;
-	public final Map<String,Object> headers = Collections.synchronizedMap(new LinkedHashMap<String,Object>());
+	public final Map<String,Object> headers = Collections.synchronizedMap(new CaseInsensitiveMap<Object>(new LinkedHashMap<String,CaseInsensitiveMap.Value<Object>>()));
 	{
-		headers.put("server","goodjava");
+		headers.put("Server","goodjava");
 	}
 	private static final Body empty = new Body(0,new InputStream(){
 		public int read() { return -1; }
@@ -76,7 +77,7 @@
 	public static Response errorResponse(Status status,String text) {
 		Response response = new Response();
 		response.status = status;
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		response.headers.put( "Content-Type", "text/plain; charset=utf-8" );
 		PrintWriter writer = new PrintWriter( new ResponseOutputStream(response) );
 		writer.write( text );
 		writer.close();
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/examples/Cookies.java
--- a/src/goodjava/webserver/examples/Cookies.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/examples/Cookies.java	Fri Apr 30 20:23:28 2021 -0600
@@ -26,7 +26,7 @@
 				response.setCookie(name,"delete",attributes);
 			}
 		}
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		response.headers.put( "Content-Type", "text/plain; charset=utf-8" );
 		try {
 			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
 			for( Map.Entry<String,String> entry : request.cookies.entrySet() ) {
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/examples/Example.java
--- a/src/goodjava/webserver/examples/Example.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/examples/Example.java	Fri Apr 30 20:23:28 2021 -0600
@@ -23,7 +23,7 @@
 
 	public Response handle(Request request) {
 		Response response = new Response();
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		response.headers.put( "Content-Type", "text/plain; charset=utf-8" );
 		try {
 			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
 			writer.write("Hello World\n");
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/examples/Headers.java
--- a/src/goodjava/webserver/examples/Headers.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/examples/Headers.java	Fri Apr 30 20:23:28 2021 -0600
@@ -14,7 +14,7 @@
 
 	public Response handle(Request request) {
 		Response response = new Response();
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		response.headers.put( "Content-Type", "text/plain; charset=utf-8" );
 		try {
 			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
 			for( Map.Entry<String,Object> entry : request.headers.entrySet() ) {
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/examples/Params.java
--- a/src/goodjava/webserver/examples/Params.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/examples/Params.java	Fri Apr 30 20:23:28 2021 -0600
@@ -14,7 +14,7 @@
 
 	public Response handle(Request request) {
 		Response response = new Response();
-		response.headers.put( "content-type", "text/plain; charset=utf-8" );
+		response.headers.put( "Content-Type", "text/plain; charset=utf-8" );
 		try {
 			Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
 			for( Map.Entry<String,Object> entry : request.parameters.entrySet() ) {
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/handlers/ContentTypeHandler.java
--- a/src/goodjava/webserver/handlers/ContentTypeHandler.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/handlers/ContentTypeHandler.java	Fri Apr 30 20:23:28 2021 -0600
@@ -2,56 +2,59 @@
 
 import java.util.Map;
 import java.util.HashMap;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
 import goodjava.webserver.Handler;
 import goodjava.webserver.Request;
 import goodjava.webserver.Response;
 
 
 public class ContentTypeHandler implements Handler {
-	private final Handler handler;
+	private static final Logger logger = LoggerFactory.getLogger(ContentTypeHandler.class);
 
 	// maps extension to content-type
 	// key must be lower case
-	public final Map<String,String> map = new HashMap<String,String>();
-
-	// set to null for none
-	public String contentTypeForNoExtension;
-
-	public ContentTypeHandler(Handler handler) {
-		this(handler,"utf-8");
+	public static final Map<String,String> map = new HashMap<String,String>();
+	static {
+		String textType = "text/plain; charset=utf-8";
+		map.put( "txt", textType );
+		map.put( "luan", textType );
+		map.put( "luano", textType );
+		map.put( "log", textType );
+		map.put( "html", "text/html; charset=utf-8" );
+		map.put( "css", "text/css; charset=utf-8" );
+		map.put( "js", "application/javascript; charset=utf-8" );
+		map.put( "json", "application/json; charset=utf-8" );
+		map.put( "mp4", "video/mp4" );
+		map.put( "jpg", "image/jpeg" );
+		map.put( "jpeg", "image/jpeg" );
+		map.put( "png", "image/png" );
+		// add more as need
 	}
 
-	public ContentTypeHandler(Handler handler,String charset) {
+	public static String getExtension(String path) {
+		int iSlash = path.lastIndexOf('/');
+		int iDot = path.lastIndexOf('.');
+		return iDot > iSlash ? path.substring(iDot+1).toLowerCase() : null;
+	}
+
+	private final Handler handler;
+
+	public ContentTypeHandler(Handler handler) {
 		this.handler = handler;
-		String attrs = charset== null ? "" : "; charset="+charset;
-		String htmlType = "text/html" + attrs;
-		String textType = "text/plain" + attrs;
-		contentTypeForNoExtension = htmlType;
-		map.put( "html", htmlType );
-		map.put( "txt", textType );
-		map.put( "luan", textType );
-		map.put( "css", "text/css" );
-		map.put( "js", "application/javascript" );
-		map.put( "json", "application/json" + attrs );
-		map.put( "mp4", "video/mp4" );
-		// add more as need
 	}
 
 	public Response handle(Request request) {
 		Response response = handler.handle(request);
-		if( response!=null && !response.headers.containsKey("content-type") ) {
-			String path = request.path;
-			int iSlash = path.lastIndexOf('/');
-			int iDot = path.lastIndexOf('.');
-			String type;
-			if( iDot < iSlash ) {  // no extension
-				type = contentTypeForNoExtension;
-			} else {  // extension
-				String extension = path.substring(iDot+1);
-				type = map.get( extension.toLowerCase() );
+		if( response!=null && response.status.code==200 && !response.headers.containsKey("Content-Type") ) {
+			String extension = getExtension(request.path);
+			if( extension != null ) {
+				String type = map.get(extension);
+				if( type != null )
+					response.headers.put("Content-Type",type);
+				else
+					logger.info("no type defined for extension: "+extension);
 			}
-			if( type != null )
-				response.headers.put("content-type",type);
 		}
 		return response;
 	}
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/handlers/DirHandler.java
--- a/src/goodjava/webserver/handlers/DirHandler.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/handlers/DirHandler.java	Fri Apr 30 20:23:28 2021 -0600
@@ -42,7 +42,7 @@
 			if( path.endsWith("/") && file.isDirectory() ) {
 				DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
 				Response response = new Response();
-				response.headers.put( "content-type", "text/html; charset=utf-8" );
+				response.headers.put( "Content-Type", "text/html; charset=utf-8" );
 				Writer writer = new OutputStreamWriter( new ResponseOutputStream(response) );
 				writer.write( "<!doctype html>\n<html>\n" );
 				writer.write( "\t<head>\n" );
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/handlers/FileHandler.java
--- a/src/goodjava/webserver/handlers/FileHandler.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/handlers/FileHandler.java	Fri Apr 30 20:23:28 2021 -0600
@@ -44,7 +44,7 @@
 				DateFormat fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
 				fmt.setTimeZone(TimeZone.getTimeZone("GMT"));
 				String lastModified = fmt.format(new Date(file.lastModified()));
-				String ifMod = (String)request.headers.get("if-modified-since");
+				String ifMod = (String)request.headers.get("If-Modified-Since");
 				if( ifMod != null ) {
 					try {
 						Date ifModDate = fmt.parse(ifMod);
@@ -55,7 +55,7 @@
 
 					} catch(ParseException e) {}
 				}
-				response.headers.put("last-modified",lastModified);
+				response.headers.put("Last-Modified",lastModified);
 
 				response.body = new Response.Body( file.length(), new FileInputStream(file) );
 				return response;
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/handlers/HeadersHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/handlers/HeadersHandler.java	Fri Apr 30 20:23:28 2021 -0600
@@ -0,0 +1,35 @@
+package goodjava.webserver.handlers;
+
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+
+
+public class HeadersHandler implements Handler {
+	private final Handler handler;
+
+	public HeadersHandler(Handler handler) {
+		this.handler = handler;
+	}
+
+	public Response handle(Request request) {
+		Response response = handler.handle(request);
+		if( response!=null ) {
+			if( response.headers.get("Last-Modified")!=null
+				&& response.headers.get("Cache-Control")==null
+			) {
+				String contentType = (String)response.headers.get("Content-Type");
+				if( contentType!=null
+					&& !contentType.startsWith("image/")
+					&& !contentType.startsWith("video/")
+				)
+					response.headers.put("Cache-Control","max-age=3600");
+			}
+			if( response.headers.get("Last-Modified")!=null
+				&& response.headers.get("X-Accel-Expires")==null
+			)
+				response.headers.put("X-Accel-Expires","1");
+		}
+		return response;
+	}
+}
diff -r 7c7f28c724e8 -r fa066aaa068c src/goodjava/webserver/handlers/SafeHandler.java
--- a/src/goodjava/webserver/handlers/SafeHandler.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/goodjava/webserver/handlers/SafeHandler.java	Fri Apr 30 20:23:28 2021 -0600
@@ -31,7 +31,7 @@
 			logger.error("",e);
 			Response response = new Response();
 			response.status = Status.INTERNAL_SERVER_ERROR;
-			response.headers.put( "content-type", "text/plain; charset=utf-8" );
+			response.headers.put( "Content-Type", "text/plain; charset=utf-8" );
 			PrintWriter writer = new PrintWriter( new ResponseOutputStream(response) );
 			writer.write( "Internel Server Error\n\n" );
 			e.printStackTrace(writer);
diff -r 7c7f28c724e8 -r fa066aaa068c src/luan/LuanTable.java
--- a/src/luan/LuanTable.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/luan/LuanTable.java	Fri Apr 30 20:23:28 2021 -0600
@@ -105,10 +105,9 @@
 	}
 
 	public String toStringLuan(Luan luan) throws LuanException {
-		Object h = getHandler(luan,"__to_string");
-		if( h == null )
+		LuanFunction fn = luan.getHandlerFunction("__to_string",this);
+		if( fn == null )
 			return rawToString();
-		LuanFunction fn = Luan.checkFunction(h);
 		return Luan.checkString( Luan.first( fn.call(luan,this) ) );
 	}
 
@@ -268,9 +267,8 @@
 	}
 
 	public int length(Luan luan) throws LuanException {
-		Object h = getHandler(luan,"__len");
-		if( h != null ) {
-			LuanFunction fn = Luan.checkFunction(h);
+		LuanFunction fn = luan.getHandlerFunction("__len",this);
+		if( fn != null ) {
 			return (Integer)Luan.first(fn.call(luan,this));
 		}
 		return rawLength();
@@ -299,9 +297,10 @@
 	}
 
 	public Iterator<Map.Entry> iterator(final Luan luan) throws LuanException {
-		if( getHandler(luan,"__pairs") == null )
+		LuanFunction h = luan.getHandlerFunction("__pairs",this);
+		if( h == null )
 			return rawIterator();
-		final LuanFunction fn = pairs(luan);
+		final LuanFunction fn = pairs(luan,h);
 		return new Iterator<Map.Entry>() {
 			private Map.Entry<Object,Object> next = getNext();
 
@@ -336,18 +335,16 @@
 	}
 
 	public LuanFunction pairs(Luan luan) throws LuanException {
-		Object h = getHandler(luan,"__pairs");
-		if( h != null ) {
-			if( h instanceof LuanFunction ) {
-				LuanFunction fn = (LuanFunction)h;
-				Object obj = Luan.first(fn.call(luan,this));
-				if( !(obj instanceof LuanFunction) )
-					throw new LuanException( "metamethod __pairs should return function but returned " + Luan.type(obj) );
-				return (LuanFunction)obj;
-			}
-			throw new LuanException( "invalid type of metamethod __pairs: " + Luan.type(h) );
-		}
-		return rawPairs();
+		return pairs( luan, luan.getHandlerFunction("__pairs",this) );
+	}
+
+	private LuanFunction pairs(Luan luan,LuanFunction fn) throws LuanException {
+		if( fn==null )
+			return rawPairs();
+		Object obj = Luan.first(fn.call(luan,this));
+		if( !(obj instanceof LuanFunction) )
+			throw new LuanException( "metamethod __pairs should return function but returned " + Luan.type(obj) );
+		return (LuanFunction)obj;
 	}
 
 	private LuanFunction rawPairs() {
diff -r 7c7f28c724e8 -r fa066aaa068c src/luan/host/WebHandler.java
--- a/src/luan/host/WebHandler.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/luan/host/WebHandler.java	Fri Apr 30 20:23:28 2021 -0600
@@ -16,6 +16,7 @@
 import goodjava.webserver.handlers.LogHandler;
 import goodjava.webserver.handlers.FileHandler;
 import goodjava.webserver.handlers.DirHandler;
+import goodjava.webserver.handlers.HeadersHandler;
 import luan.Luan;
 import luan.LuanException;
 import luan.LuanTable;
@@ -65,11 +66,13 @@
 
 			FileHandler fileHandler = new FileHandler(dirStr+"/site/");
 			Handler handler = new ListHandler( luanHandler, fileHandler );
+			handler = new ContentTypeHandler(handler);
 			handler = new IndexHandler(handler);
 			DirHandler dirHandler = new DirHandler(fileHandler);
 			Handler notFoundHander = new NotFound(luanHandler);
+			notFoundHander = new ContentTypeHandler(notFoundHander);
 			handler = new ListHandler( handler, dirHandler, notFoundHander );
-			handler = new ContentTypeHandler(handler);
+			handler = new HeadersHandler(handler);
 			handler = new SafeHandler(handler);
 			handler = new LogHandler(handler,LogHandler.dirLogger(new File(logDir),days30));
 
diff -r 7c7f28c724e8 -r fa066aaa068c src/luan/modules/BasicLuan.java
--- a/src/luan/modules/BasicLuan.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/luan/modules/BasicLuan.java	Fri Apr 30 20:23:28 2021 -0600
@@ -211,8 +211,9 @@
 		}
 	}
 
-	public static String stringify(Object obj,LuanTable options,LuanTable subOptions) throws LuanException {
+	public static String stringify(Luan luan,Object obj,LuanTable options,LuanTable subOptions) throws LuanException {
 		LuanToString lts = new LuanToString(options,subOptions);
+		lts.luan = luan;
 		return lts.toString(obj);
 	}
 
diff -r 7c7f28c724e8 -r fa066aaa068c src/luan/modules/Table.luan
--- a/src/luan/modules/Table.luan	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/luan/modules/Table.luan	Fri Apr 30 20:23:28 2021 -0600
@@ -20,6 +20,7 @@
 local error = Luan.error
 local type = Luan.type or error()
 local pairs = Luan.pairs or error()
+local set_metatable = Luan.set_metatable or error()
 local toTable = TableLuan.toTable or error()
 local copy = Table.copy or error()
 
@@ -56,4 +57,41 @@
 end
 
 
+function Table.case_insensitive(src)
+	local String = require "luan:String.luan"
+	local lower = String.lower or error()
+
+	local map = {}
+	local mt = {}
+	function mt.__new_index(tbl,key,value)
+		if value==nil then
+			map[lower(key)] = nil
+		else
+			map[lower(key)] = { s=key, v=value }
+		end
+	end
+	function mt.__index(tbl,key)
+		local val = map[lower(key)]
+		return val and val.v
+	end
+	function mt.__pairs(tbl)
+		local fn = pairs(map)
+		return function()
+			local _, val = fn()
+			if val == nil then
+				return nil
+			end
+			return val.s, val.v
+		end
+	end
+
+	local t = {}
+	set_metatable(t,mt)
+	for k,v in pairs(src or {}) do
+		t[k] = v
+	end
+	return t
+end
+
+
 return Table
diff -r 7c7f28c724e8 -r fa066aaa068c src/luan/modules/http/Http.luan
--- a/src/luan/modules/http/Http.luan	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/luan/modules/http/Http.luan	Fri Apr 30 20:23:28 2021 -0600
@@ -15,6 +15,7 @@
 local Table = require "luan:Table.luan"
 local clear = Table.clear or error()
 local java_to_table_deep = Table.java_to_table_deep or error()
+local case_insensitive = Table.case_insensitive or error()
 local Package = require "luan:Package.luan"
 local String = require "luan:String.luan"
 local lower = String.lower or error()
@@ -75,7 +76,7 @@
 	if java == nil then
 		this.method = "GET"
 		this.scheme = "http"
-		this.headers = {}
+		this.headers = case_insensitive{}
 		this.parameters = {}
 		this.cookies = {}
 	else
@@ -88,13 +89,13 @@
 		this.path = java.path or error()
 		this.protocol = java.protocol or error()
 		this.scheme = java.scheme or error()
-		this.headers = java_to_table_deep(java.headers)
+		this.headers = case_insensitive(java_to_table_deep(java.headers))
 		this.parameters = java_to_table_deep(java.parameters,java_to_table_shallow)
 		this.cookies = java_to_table_deep(java.cookies)
 	end
 
 	function this.url()
-		return this.scheme.."://"..this.headers["host"]..this.raw_path
+		return this.scheme.."://"..this.headers["Host"]..this.raw_path
 	end
 
 	return this
@@ -115,7 +116,7 @@
 
 	function this.reset()
 		this.java = Response.new()
-		this.headers = {}
+		this.headers = case_insensitive{}
 		this.status = STATUS.OK
 		this.writer = nil
 	end
@@ -124,14 +125,14 @@
 
 	function this.send_redirect(location)
 		this.status = STATUS.FOUND
-		this.headers["location"] = location
+		this.headers["Location"] = location
 	end
 
 	function this.send_error(status,msg)
 		this.reset()
 		this.status = status
 		if msg ~= nil then
-			this.headers["content-type"] = "text/plain; charset=utf-8"
+			this.headers["Content-Type"] = "text/plain; charset=utf-8"
 			local writer = this.text_writer()
 			writer.write(msg)
 		end
@@ -183,7 +184,6 @@
 	java.status = Status.getStatus(response.status)
 	for name, value in pairs(response.headers) do
 		type(name)=="string" or "header name must be string"
-		name = lower(name)
 		value = LuanJava.toJava(value)
 		java.headers.put(name,value)
 	end
@@ -209,7 +209,7 @@
 Http.is_serving = false
 
 function Http.format_date(date)
-	return time_format(date,"EEE, dd MMM yyyy HH:mm:ss 'GMT'","GMT")
+	return time_format(date,"EEE, dd MMM yyyy HH:mm:ss z","GMT")
 end
 
 return Http
diff -r 7c7f28c724e8 -r fa066aaa068c src/luan/modules/http/Server.luan
--- a/src/luan/modules/http/Server.luan	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/luan/modules/http/Server.luan	Fri Apr 30 20:23:28 2021 -0600
@@ -22,6 +22,7 @@
 local SafeHandler = require "java:goodjava.webserver.handlers.SafeHandler"
 local LogHandler = require "java:goodjava.webserver.handlers.LogHandler"
 local ListHandler = require "java:goodjava.webserver.handlers.ListHandler"
+local HeadersHandler = require "java:goodjava.webserver.handlers.HeadersHandler"
 local LuanHandler = require "java:luan.modules.http.LuanHandler"
 local System = require "java:java.lang.System"
 local NotFound = require "java:luan.modules.http.NotFound"
@@ -67,11 +68,13 @@
 	local file_handler = FileHandler.new(dir_path)
 	local luan_handler = LuanHandler.new()
 	local handler = ListHandler.new( luan_handler, file_handler )
+	handler = ContentTypeHandler.new(handler)
 	handler = IndexHandler.new(handler)
 	local dir_handler = DirHandler.new(file_handler)
 	local not_found_hander = NotFound.new(luan_handler)
+	not_found_hander = ContentTypeHandler.new(not_found_hander)
 	handler = ListHandler.new( handler, dir_handler, not_found_hander )
-	handler = ContentTypeHandler.new(handler)
+	handler = HeadersHandler.new(handler)
 	handler = SafeHandler.new(handler)
 	handler = LogHandler.new(handler)
 	local server = JavaServer.new(port,handler)
diff -r 7c7f28c724e8 -r fa066aaa068c src/luan/modules/http/tools/Run.luan
--- a/src/luan/modules/http/tools/Run.luan	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/luan/modules/http/tools/Run.luan	Fri Apr 30 20:23:28 2021 -0600
@@ -26,7 +26,9 @@
 	end
 end
 
-local function form() %>
+local function form()
+	Http.response.headers["Content-Type"] = "text/html; charset=utf-8"
+%>
 <!doctype html>
 <html>
 	<head>
@@ -54,7 +56,6 @@
 	<body>
 		<h2>Run Luan Code</h2>
 		<form method="post">
-			<input type="hidden" name="content_type" value="text/plain; charset=utf-8" />
 			<div>
 				<textarea name="code" rows="20" cols="90" autofocus></textarea>
 			</div>
@@ -64,16 +65,18 @@
 		</form>
 	</body>
 </html>
-<% end
+<% 
+end
 
 function Run.run(code,source_name)
 	try
+		Http.response.headers["Content-Type"] = "text/plain; charset=utf-8"
 		local run = load(code,source_name)
 		run()
 		return true
 	catch e
 		Http.response.reset()
-		Http.response.headers["content-type"] = "text/plain; charset=utf-8"
+		Http.response.headers["Content-Type"] = "text/plain; charset=utf-8"
 		Io.stdout = Http.response.text_writer()
 		print(e)
 		print()
@@ -84,10 +87,6 @@
 end
 
 function Run.respond()
-	local content_type = Http.request.parameters.content_type
-	if content_type ~= nil then
-		Http.response.headers["content-type"] = content_type
-	end
 	Io.stdout = Http.response.text_writer()
 	local code = Http.request.parameters.code
 	if code == nil then
diff -r 7c7f28c724e8 -r fa066aaa068c src/luan/modules/parsers/LuanToString.java
--- a/src/luan/modules/parsers/LuanToString.java	Tue Apr 20 18:06:50 2021 -0600
+++ b/src/luan/modules/parsers/LuanToString.java	Fri Apr 30 20:23:28 2021 -0600
@@ -7,6 +7,7 @@
 import java.util.Collections;
 import luan.Luan;
 import luan.LuanTable;
+import luan.LuanFunction;
 import luan.LuanException;
 import luan.LuanRuntimeException;
 
@@ -62,6 +63,7 @@
 	}
 
 	public final Settings settingsInit = new Settings();
+	public Luan luan = null;
 	private final LuanTable subOptions;
 
 	public LuanToString(LuanTable options,LuanTable subOptions) throws LuanException {
@@ -119,6 +121,46 @@
 	}
 
 	private void toString(LuanTable tbl,StringBuilder sb,int indented,Settings settings) throws LuanException {
+		if( tbl.getMetatable()!=null ) {
+			if( settings.strict )
+				throw new LuanException("can't handle metatables when strict");
+			if( luan==null )
+				throw new LuanException("can't handle metatables when luan isn't set");
+		}
+		LuanFunction pairs = luan.getHandlerFunction("__pairs",tbl);
+		if( pairs != null ) {
+			sb.append( '{' );
+			boolean first = true;
+			for( Object obj : tbl.iterable(luan) ) {
+				Map.Entry entry = (Map.Entry)obj;
+				if( settings.compressed ) {
+					if( first )
+						first = false;
+					else
+						sb.append( ',' );
+				} else if( settings.inline ) {
+					if( first ) {
+						first = false;
+						sb.append( ' ' );
+					} else
+						sb.append( ", " );
+				} else {
+					first = false;
+					indent(sb,indented+1);
+				}
+				toString(entry,sb,indented+1,settings);
+			}
+			if( !first ) {
+				if( settings.compressed ) {
+				} else if( settings.inline ) {
+					sb.append( ' ' );
+				} else {
+					indent(sb,indented);
+				}
+			}
+			sb.append( '}' );
+			return;
+		}
 		List list = tbl.asList();
 		Map map = tbl.rawMap();
 		sb.append( '{' );