diff --git a/main.go b/main.go
index 8a1602eb10bfb651469940dabfabab41515a7a4f..b2ff1e1440afc20b9759f4582e1097a70071421d 100644
--- a/main.go
+++ b/main.go
@@ -52,7 +52,7 @@ const gitProjectPattern = `^/[^/]+/[^/]+\.git/`
 const apiPattern = `^/api/`
 
 // A project ID in an API request is either a number or two strings 'namespace/project'
-const projectsAPIPattern = `^/api/v3/projects/(\d+)|([^/]+/[^/]+)/`
+const projectsAPIPattern = `^/api/v3/projects/((\d+)|([^/]+/[^/]+))/`
 
 const ciAPIPattern = `^/ci/api/`
 
diff --git a/main_test.go b/main_test.go
index ba0f0c288915715e3cc0d98d5a7d075f31fd319d..1990f73ddd71c693d08b4a1f1e49b6bf848879cd 100644
--- a/main_test.go
+++ b/main_test.go
@@ -273,6 +273,37 @@ func TestDownloadCacheCreate(t *testing.T) {
 	}
 }
 
+func TestRegularProjectsAPI(t *testing.T) {
+	apiResponse := "API RESPONSE"
+	ts := testAuthServer(nil, 200, apiResponse)
+	defer ts.Close()
+	ws := startWorkhorseServer(ts.URL)
+	defer ws.Close()
+
+	for _, resource := range []string{
+		"/api/v3/projects/123/repository/not/special",
+		"/api/v3/projects/foo%2Fbar/repository/not/special",
+		"/api/v3/projects/123/not/special",
+		"/api/v3/projects/foo%2Fbar/not/special",
+	} {
+		resp, err := http.Get(ws.URL + resource)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer resp.Body.Close()
+		buf := &bytes.Buffer{}
+		if _, err := io.Copy(buf, resp.Body); err != nil {
+			t.Error(err)
+		}
+		if buf.String() != apiResponse {
+			t.Errorf("GET %q: Expected %q, got %q", resource, apiResponse, buf.String())
+		}
+		if resp.StatusCode != 200 {
+			t.Errorf("GET %q: expected 200, got %d", resource, resp.StatusCode)
+		}
+	}
+}
+
 func TestAllowedXSendfileDownload(t *testing.T) {
 	contentFilename := "my-content"
 	prepareDownloadDir(t)