diff --git a/misc/dashboard/builder/http.go b/misc/dashboard/builder/http.go
index 4546f855a4f3872662b8b5f84911e1d146475d6e..5e1da0c878d45a8293ab5ddba59f6f86904120af 100644
--- a/misc/dashboard/builder/http.go
+++ b/misc/dashboard/builder/http.go
@@ -26,18 +26,18 @@ func dash(meth, cmd string, resp interface{}, args param) os.Error {
 		log.Println("dash", cmd, args)
 	}
 	cmd = "http://" + *dashboard + "/" + cmd
+	vals := make(http.Values)
+	for k, v := range args {
+		vals.Add(k, v)
+	}
 	switch meth {
 	case "GET":
-		if args != nil {
-			m := make(map[string][]string)
-			for k, v := range args {
-				m[k] = []string{v}
-			}
-			cmd += "?" + http.EncodeQuery(m)
+		if q := vals.Encode(); q != "" {
+			cmd += "?" + q
 		}
 		r, err = http.Get(cmd)
 	case "POST":
-		r, err = http.PostForm(cmd, args)
+		r, err = http.PostForm(cmd, vals)
 	default:
 		return fmt.Errorf("unknown method %q", meth)
 	}
diff --git a/src/pkg/http/client.go b/src/pkg/http/client.go
index 7e1d65df30ba3086bc5e8ba085acc87b03968115..71b0370422e53adbfc515733ef070daf80cff016 100644
--- a/src/pkg/http/client.go
+++ b/src/pkg/http/client.go
@@ -7,7 +7,6 @@
 package http
 
 import (
-	"bytes"
 	"encoding/base64"
 	"fmt"
 	"io"
@@ -240,7 +239,7 @@ func (c *Client) Post(url string, bodyType string, body io.Reader) (r *Response,
 // Caller should close r.Body when done reading from it.
 //
 // PostForm is a wrapper around DefaultClient.PostForm
-func PostForm(url string, data map[string]string) (r *Response, err os.Error) {
+func PostForm(url string, data Values) (r *Response, err os.Error) {
 	return DefaultClient.PostForm(url, data)
 }
 
@@ -248,17 +247,8 @@ func PostForm(url string, data map[string]string) (r *Response, err os.Error) {
 // with data's keys and values urlencoded as the request body.
 //
 // Caller should close r.Body when done reading from it.
-func (c *Client) PostForm(url string, data map[string]string) (r *Response, err os.Error) {
-	return c.Post(url, "application/x-www-form-urlencoded", urlencode(data))
-}
-
-// TODO: remove this function when PostForm takes a multimap.
-func urlencode(data map[string]string) (b *bytes.Buffer) {
-	m := make(map[string][]string, len(data))
-	for k, v := range data {
-		m[k] = []string{v}
-	}
-	return bytes.NewBuffer([]byte(EncodeQuery(m)))
+func (c *Client) PostForm(url string, data Values) (r *Response, err os.Error) {
+	return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
 }
 
 // Head issues a HEAD to the specified URL.  If the response is one of the
diff --git a/src/pkg/http/client_test.go b/src/pkg/http/client_test.go
index 822a8889cad7d42f6aa5e110fd57a0cd63fb5541..9ef81d9d4f679ccfb4e408e422a71c6af7950523 100644
--- a/src/pkg/http/client_test.go
+++ b/src/pkg/http/client_test.go
@@ -109,7 +109,10 @@ func TestPostFormRequestFormat(t *testing.T) {
 	client := &Client{Transport: tr}
 
 	url := "http://dummy.faketld/"
-	form := map[string]string{"foo": "bar"}
+	form := make(Values)
+	form.Set("foo", "bar")
+	form.Add("foo", "bar2")
+	form.Set("bar", "baz")
 	client.PostForm(url, form) // Note: doesn't hit network
 
 	if tr.req.Method != "POST" {
@@ -127,10 +130,17 @@ func TestPostFormRequestFormat(t *testing.T) {
 	if tr.req.Close {
 		t.Error("got Close true, want false")
 	}
-	if g, e := tr.req.ContentLength, int64(len("foo=bar")); g != e {
+	expectedBody := "foo=bar&foo=bar2&bar=baz"
+	if g, e := tr.req.ContentLength, int64(len(expectedBody)); g != e {
 		t.Errorf("got ContentLength %d, want %d", g, e)
 	}
-
+	bodyb, err := ioutil.ReadAll(tr.req.Body)
+	if err != nil {
+		t.Fatalf("ReadAll on req.Body: %v", err)
+	}
+	if g := string(bodyb); g != expectedBody {
+		t.Errorf("got body %q, want %q", g, expectedBody)
+	}
 }
 
 func TestRedirects(t *testing.T) {
diff --git a/src/pkg/http/readrequest_test.go b/src/pkg/http/readrequest_test.go
index 19e2ff77476a97581c0f7f151b0af4cef8a750d8..d93e573f58bd4fad4dac0887ecabfc5b3a6a1d10 100644
--- a/src/pkg/http/readrequest_test.go
+++ b/src/pkg/http/readrequest_test.go
@@ -64,7 +64,7 @@ var reqTests = []reqTest{
 			Host:          "www.techcrunch.com",
 			Referer:       "",
 			UserAgent:     "Fake",
-			Form:          map[string][]string{},
+			Form:          Values{},
 		},
 
 		"abcdef\n",
@@ -99,7 +99,7 @@ var reqTests = []reqTest{
 			Host:          "test",
 			Referer:       "",
 			UserAgent:     "",
-			Form:          map[string][]string{},
+			Form:          Values{},
 		},
 
 		"",
diff --git a/src/pkg/http/request.go b/src/pkg/http/request.go
index 2f6b651c3e3b80d99d845918b598dec7a47df896..2ff3160a95036a671ee6ecddc81b125ebb3bfb7a 100644
--- a/src/pkg/http/request.go
+++ b/src/pkg/http/request.go
@@ -90,10 +90,10 @@ type Request struct {
 	//
 	// then
 	//
-	//	Header = map[string]string{
-	//		"Accept-Encoding": "gzip, deflate",
-	//		"Accept-Language": "en-us",
-	//		"Connection": "keep-alive",
+	//	Header = map[string][]string{
+	//		"Accept-Encoding": {"gzip, deflate"},
+	//		"Accept-Language": {"en-us"},
+	//		"Connection": {"keep-alive"},
 	//	}
 	//
 	// HTTP defines that header names are case-insensitive.
@@ -141,7 +141,7 @@ type Request struct {
 	UserAgent string
 
 	// The parsed form. Only available after ParseForm is called.
-	Form map[string][]string
+	Form Values
 
 	// The parsed multipart form, including file uploads.
 	// Only available after ParseMultipartForm is called.
@@ -597,18 +597,56 @@ func ReadRequest(b *bufio.Reader) (req *Request, err os.Error) {
 	return req, nil
 }
 
+// Values maps a string key to a list of values.
+// It is typically used for query parameters and form values.
+// Unlike in the Header map, the keys in a Values map
+// are case-sensitive.
+type Values map[string][]string
+
+// Get gets the first value associated with the given key.
+// If there are no values associated with the key, Get returns
+// the empty string. To access multiple values, use the map
+// directly.
+func (v Values) Get(key string) string {
+	if v == nil {
+		return ""
+	}
+	vs, ok := v[key]
+	if !ok || len(vs) == 0 {
+		return ""
+	}
+	return vs[0]
+}
+
+// Set sets the key to value. It replaces any existing
+// values.
+func (v Values) Set(key, value string) {
+	v[key] = []string{value}
+}
+
+// Add adds the key to value. It appends to any existing
+// values associated with key.
+func (v Values) Add(key, value string) {
+	v[key] = append(v[key], value)
+}
+
+// Del deletes the values associated with key.
+func (v Values) Del(key string) {
+	v[key] = nil, false
+}
+
 // ParseQuery parses the URL-encoded query string and returns
 // a map listing the values specified for each key.
 // ParseQuery always returns a non-nil map containing all the
 // valid query parameters found; err describes the first decoding error
 // encountered, if any.
-func ParseQuery(query string) (m map[string][]string, err os.Error) {
-	m = make(map[string][]string)
+func ParseQuery(query string) (m Values, err os.Error) {
+	m = make(Values)
 	err = parseQuery(m, query)
 	return
 }
 
-func parseQuery(m map[string][]string, query string) (err os.Error) {
+func parseQuery(m Values, query string) (err os.Error) {
 	for _, kv := range strings.Split(query, "&", -1) {
 		if len(kv) == 0 {
 			continue
@@ -641,7 +679,7 @@ func (r *Request) ParseForm() (err os.Error) {
 		return
 	}
 
-	r.Form = make(map[string][]string)
+	r.Form = make(Values)
 	if r.URL != nil {
 		err = parseQuery(r.Form, r.URL.RawQuery)
 	}
diff --git a/src/pkg/http/url.go b/src/pkg/http/url.go
index d7ee14ee84ae3702b79e4615f7e3a722bd39c11a..05b1662d381a30bc4c7e303bf02843733fa356a6 100644
--- a/src/pkg/http/url.go
+++ b/src/pkg/http/url.go
@@ -486,10 +486,14 @@ func (url *URL) String() string {
 	return result
 }
 
-// EncodeQuery encodes the query represented as a multimap.
-func EncodeQuery(m map[string][]string) string {
-	parts := make([]string, 0, len(m)) // will be large enough for most uses
-	for k, vs := range m {
+// Encode encodes the values into ``URL encoded'' form.
+// e.g. "foo=bar&bar=baz"
+func (v Values) Encode() string {
+	if v == nil {
+		return ""
+	}
+	parts := make([]string, 0, len(v)) // will be large enough for most uses
+	for k, vs := range v {
 		prefix := URLEscape(k) + "="
 		for _, v := range vs {
 			parts = append(parts, prefix+URLEscape(v))
@@ -593,3 +597,9 @@ func (base *URL) ResolveReference(ref *URL) *URL {
 	url.Raw = url.String()
 	return url
 }
+
+// Query parses RawQuery and returns the corresponding values.
+func (u *URL) Query() Values {
+	v, _ := ParseQuery(u.RawQuery)
+	return v
+}
diff --git a/src/pkg/http/url_test.go b/src/pkg/http/url_test.go
index d8863f3d3b5751ad0bd695ef5c70a1a6647dbc95..eaec5872aedf8ca317295f26c21df6aecb20f424 100644
--- a/src/pkg/http/url_test.go
+++ b/src/pkg/http/url_test.go
@@ -538,23 +538,21 @@ func TestUnescapeUserinfo(t *testing.T) {
 	}
 }
 
-type qMap map[string][]string
-
 type EncodeQueryTest struct {
-	m         qMap
+	m         Values
 	expected  string
 	expected1 string
 }
 
 var encodeQueryTests = []EncodeQueryTest{
 	{nil, "", ""},
-	{qMap{"q": {"puppies"}, "oe": {"utf8"}}, "q=puppies&oe=utf8", "oe=utf8&q=puppies"},
-	{qMap{"q": {"dogs", "&", "7"}}, "q=dogs&q=%26&q=7", "q=dogs&q=%26&q=7"},
+	{Values{"q": {"puppies"}, "oe": {"utf8"}}, "q=puppies&oe=utf8", "oe=utf8&q=puppies"},
+	{Values{"q": {"dogs", "&", "7"}}, "q=dogs&q=%26&q=7", "q=dogs&q=%26&q=7"},
 }
 
 func TestEncodeQuery(t *testing.T) {
 	for _, tt := range encodeQueryTests {
-		if q := EncodeQuery(tt.m); q != tt.expected && q != tt.expected1 {
+		if q := tt.m.Encode(); q != tt.expected && q != tt.expected1 {
 			t.Errorf(`EncodeQuery(%+v) = %q, want %q`, tt.m, q, tt.expected)
 		}
 	}
@@ -673,3 +671,28 @@ func TestResolveReference(t *testing.T) {
 	}
 
 }
+
+func TestQueryValues(t *testing.T) {
+	u, _ := ParseURL("http://x.com?foo=bar&bar=1&bar=2")
+	v := u.Query()
+	if len(v) != 2 {
+		t.Errorf("got %d keys in Query values, want 2", len(v))
+	}
+	if g, e := v.Get("foo"), "bar"; g != e {
+		t.Errorf("Get(foo) = %q, want %q", g, e)
+	}
+	// Case sensitive:
+	if g, e := v.Get("Foo"), ""; g != e {
+		t.Errorf("Get(Foo) = %q, want %q", g, e)
+	}
+	if g, e := v.Get("bar"), "1"; g != e {
+		t.Errorf("Get(bar) = %q, want %q", g, e)
+	}
+	if g, e := v.Get("baz"), ""; g != e {
+		t.Errorf("Get(baz) = %q, want %q", g, e)
+	}
+	v.Del("bar")
+	if g, e := v.Get("bar"), ""; g != e {
+		t.Errorf("second Get(bar) = %q, want %q", g, e)
+	}
+}