Add HTTP error annotation.

This allows the status to be retrieved programmatically.

R=maruel@google.com
BUG=

Review-Url: https://codereview.chromium.org/2984913002
diff --git a/common/errors/tags.go b/common/errors/tags.go
index 74232b9..82c9af1 100644
--- a/common/errors/tags.go
+++ b/common/errors/tags.go
@@ -38,7 +38,7 @@
 		Value interface{}
 	}
 
-	// A TagValueGenerator generates (TagKey, value) pairs, for use with Annoatator.Tag
+	// TagValueGenerator generates (TagKey, value) pairs, for use with Annoatator.Tag
 	// and New().
 	TagValueGenerator interface {
 		GenerateErrorTagValue() TagValue
diff --git a/common/lhttp/client.go b/common/lhttp/client.go
index 217adfe..4115746 100644
--- a/common/lhttp/client.go
+++ b/common/lhttp/client.go
@@ -54,6 +54,20 @@
 // documentation.
 type RequestGen func() (*http.Request, error)
 
+var httpTagKey = errors.NewTagKey("this is an HTTP error")
+
+func applyHTTPTag(err error, status int) error {
+	return errors.TagValue{Key: httpTagKey, Value: status}.Apply(err)
+}
+
+func IsHTTPError(err error) (status int, ok bool) {
+	d, ok := errors.TagValueIn(httpTagKey, err)
+	if ok {
+		status = d.(int)
+	}
+	return
+}
+
 // NewRequest returns a retriable request.
 //
 // The handler func is responsible for closing the response Body before
@@ -117,10 +131,11 @@
 				return handler(resp)
 			}
 
+			err = applyHTTPTag(err, status)
 			return errorHandler(resp, err)
 		}, nil)
 		if err != nil {
-			err = fmt.Errorf("%v (attempts: %d)", err, attempts)
+			err = errors.Annotate(err, "gave up after %d attempts", attempts).Err()
 		}
 		return status, err
 	}
diff --git a/common/lhttp/client_test.go b/common/lhttp/client_test.go
index 81f5a64..ae42629 100644
--- a/common/lhttp/client_test.go
+++ b/common/lhttp/client_test.go
@@ -140,7 +140,7 @@
 		}, nil)
 
 		status, err := clientReq()
-		So(err.Error(), ShouldResemble, "http request failed: Internal Server Error (HTTP 500) (attempts: 4)")
+		So(err.Error(), ShouldResemble, "gave up after 4 attempts: http request failed: Internal Server Error (HTTP 500)")
 		So(status, ShouldResemble, 500)
 	})
 }
@@ -187,7 +187,7 @@
 
 		actual := map[string]string{}
 		status, err := GetJSON(ctx, fast, http.DefaultClient, ts.URL, &actual)
-		So(err.Error(), ShouldResemble, "bad response "+ts.URL+": invalid character 'y' looking for beginning of value (attempts: 4)")
+		So(err.Error(), ShouldResemble, "gave up after 4 attempts: bad response "+ts.URL+": invalid character 'y' looking for beginning of value")
 		So(status, ShouldResemble, 200)
 		So(actual, ShouldResemble, map[string]string{})
 	})
@@ -207,7 +207,7 @@
 		defer ts.Close()
 
 		status, err := GetJSON(ctx, fast, http.DefaultClient, ts.URL, nil)
-		So(err.Error(), ShouldResemble, "bad response "+ts.URL+": invalid character 'y' looking for beginning of value (attempts: 4)")
+		So(err.Error(), ShouldResemble, "gave up after 4 attempts: bad response "+ts.URL+": invalid character 'y' looking for beginning of value")
 		So(status, ShouldResemble, 200)
 	})
 }
@@ -223,7 +223,7 @@
 		defer ts.Close()
 
 		status, err := GetJSON(ctx, fast, http.DefaultClient, ts.URL, nil)
-		So(err.Error(), ShouldResemble, "unexpected Content-Type, expected \"application/json\", got \"text/plain; charset=utf-8\" (attempts: 4)")
+		So(err.Error(), ShouldResemble, "gave up after 4 attempts: unexpected Content-Type, expected \"application/json\", got \"text/plain; charset=utf-8\"")
 		So(status, ShouldResemble, 200)
 	})
 }