theonedev/onedev · error · ClientException

405

Error message

405

What it means

The PyPI pack handler received a non-POST request on the upload endpoint. Only POST (multipart upload) is allowed there; any other method throws ClientException(SC_METHOD_NOT_ALLOWED), which the client sees as HTTP 405.

Source

Thrown at server-plugin/server-plugin-pack-pypi/src/main/java/io/onedev/server/plugin/pack/pypi/PypiPackHandler.java:191

									} 
									data.getSha256BlobHashes().put(fileName, sha256Hash);
									
									var packBlobs = data.getSha256BlobHashes().values().stream()
											.map(hash -> packBlobService.findBySha256Hash(projectId, hash))
											.filter(Objects::nonNull)
											.collect(toList());
									packService.createOrUpdate(pack, packBlobs, data.getSha256BlobHashes().size() == 1);									
								}));
								response.setStatus(SC_OK);
								break;
							}
						}
					}
				} catch (IOException | FileUploadException e) {
					throw new RuntimeException(e);
				}
			} else {
				throw new ClientException(SC_METHOD_NOT_ALLOWED);
			}
		} else {
			if (!isGet)
				throw new ClientException(SC_METHOD_NOT_ALLOWED);
			var currentSegment = pathSegments.get(0);
			pathSegments = pathSegments.subList(1, pathSegments.size());
			
			// https://peps.python.org/pep-0503/
			if (currentSegment.equals("simple")) { 
				if (pathSegments.isEmpty()) {
					sessionService.run(() -> {
						var project = checkProject(projectId, false);
						var names = packService.queryNames(project, TYPE, null, true, 0, MAX_VALUE);
						var bindings = new HashMap<String, Object>();
						bindings.put("names", names);
						try {
							URL tplUrl = Resources.getResource(getClass(), "packages.tpl");
							String template = Resources.toString(tplUrl, UTF_8);

View on GitHub (pinned to d44925c47c)

Solutions

  1. Use POST with multipart/form-data (or the standard twine upload command) against the upload endpoint
  2. Switch the request method to POST in the script or curl command (curl -X POST --form ...)
  3. Point read-only checks at the /simple index endpoint instead, which accepts GET
  4. Check any reverse-proxy rewrite rules that might change the HTTP method

Example fix

// before
curl -X PUT https://onedev.example.com/myproj/~pypi/upload -F file=mypkg.whl
// after
curl -X POST https://onedev.example.com/myproj/~pypi/upload -F "content=@mypkg.whl"
Defensive patterns

Strategy: validation

Validate before calling

// ensure the upload request uses POST
if (httpRequest.method !== 'POST') {
  throw new Error('PyPI upload endpoint requires POST, got ' + httpRequest.method);
}

Try / catch

try {
  await upload(url, formData);
} catch (e) {
  if (e.status === 405) {
    console.error('Wrong HTTP method for the PyPI endpoint; use POST for upload, GET for /simple', e);
  } else { throw e; }
}

Prevention

When it happens

Trigger: Issuing GET/PUT/DELETE against the project's ~pypi upload URL while the request is not a GET handled by the simple/index branch — i.e. wrong HTTP verb used for the upload route (PypiPackHandler.java:190-191).

Common situations: curl-ing the upload URL to 'test' it with GET; a misconfigured CI script using PUT instead of twine's POST; a health-check probe hitting the upload endpoint with HEAD/GET.

Related errors


AI-assisted analysis of theonedev/onedev@d44925c47c (2026-09-06). Data as JSON: /api/errors/5bc943b2eac14f5b. Report an issue: GitHub.