diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go
index a29bce66a4..59fd83e3a2 100644
--- a/routers/api/v1/repo/release_attachment.go
+++ b/routers/api/v1/repo/release_attachment.go
@@ -4,7 +4,9 @@
 package repo
 
 import (
+	"io"
 	"net/http"
+	"strings"
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/log"
@@ -154,6 +156,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
 	// - application/json
 	// consumes:
 	// - multipart/form-data
+	// - application/octet-stream
 	// parameters:
 	// - name: owner
 	//   in: path
@@ -180,7 +183,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
 	//   in: formData
 	//   description: attachment to upload
 	//   type: file
-	//   required: true
+	//   required: false
 	// responses:
 	//   "201":
 	//     "$ref": "#/responses/Attachment"
@@ -202,20 +205,36 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
 	}
 
 	// Get uploaded file from request
-	file, header, err := ctx.Req.FormFile("attachment")
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetFile", err)
-		return
-	}
-	defer file.Close()
+	var content io.ReadCloser
+	var filename string
+	var size int64 = -1
 
-	filename := header.Filename
-	if query := ctx.FormString("name"); query != "" {
-		filename = query
+	if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") {
+		file, header, err := ctx.Req.FormFile("attachment")
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "GetFile", err)
+			return
+		}
+		defer file.Close()
+
+		content = file
+		size = header.Size
+		filename = header.Filename
+		if name := ctx.FormString("name"); name != "" {
+			filename = name
+		}
+	} else {
+		content = ctx.Req.Body
+		filename = ctx.FormString("name")
+	}
+
+	if filename == "" {
+		ctx.Error(http.StatusBadRequest, "CreateReleaseAttachment", "Could not determine name of attachment.")
+		return
 	}
 
 	// Create a new attachment and save the file
-	attach, err := attachment.UploadAttachment(ctx, file, setting.Repository.Release.AllowedTypes, header.Size, &repo_model.Attachment{
+	attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
 		Name:       filename,
 		UploaderID: ctx.Doer.ID,
 		RepoID:     ctx.Repo.Repository.ID,
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index eab3d0b142..0fd51e4fa5 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -39,14 +39,14 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R
 }
 
 // UploadAttachment upload new attachment into storage and update database
-func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, opts *repo_model.Attachment) (*repo_model.Attachment, error) {
+func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
 	buf := make([]byte, 1024)
 	n, _ := util.ReadAtMost(file, buf)
 	buf = buf[:n]
 
-	if err := upload.Verify(buf, opts.Name, allowedTypes); err != nil {
+	if err := upload.Verify(buf, attach.Name, allowedTypes); err != nil {
 		return nil, err
 	}
 
-	return NewAttachment(ctx, opts, io.MultiReader(bytes.NewReader(buf), file), fileSize)
+	return NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), file), fileSize)
 }
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index d4c5d9a7ee..b739bea60d 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -12343,7 +12343,8 @@
       },
       "post": {
         "consumes": [
-          "multipart/form-data"
+          "multipart/form-data",
+          "application/octet-stream"
         ],
         "produces": [
           "application/json"
@@ -12386,8 +12387,7 @@
             "type": "file",
             "description": "attachment to upload",
             "name": "attachment",
-            "in": "formData",
-            "required": true
+            "in": "formData"
           }
         ],
         "responses": {
diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go
index 5b1ab76ce9..49aa4c4e1b 100644
--- a/tests/integration/api_releases_test.go
+++ b/tests/integration/api_releases_test.go
@@ -262,24 +262,60 @@ func TestAPIUploadAssetRelease(t *testing.T) {
 
 	filename := "image.png"
 	buff := generateImg()
-	body := &bytes.Buffer{}
 
-	writer := multipart.NewWriter(body)
-	part, err := writer.CreateFormFile("attachment", filename)
-	assert.NoError(t, err)
-	_, err = io.Copy(part, &buff)
-	assert.NoError(t, err)
-	err = writer.Close()
-	assert.NoError(t, err)
+	assetURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner.Name, repo.Name, r.ID)
 
-	req := NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset", owner.Name, repo.Name, r.ID), body).
-		AddTokenAuth(token)
-	req.Header.Add("Content-Type", writer.FormDataContentType())
-	resp := MakeRequest(t, req, http.StatusCreated)
+	t.Run("multipart/form-data", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
 
-	var attachment *api.Attachment
-	DecodeJSON(t, resp, &attachment)
+		body := &bytes.Buffer{}
 
-	assert.EqualValues(t, "test-asset", attachment.Name)
-	assert.EqualValues(t, 104, attachment.Size)
+		writer := multipart.NewWriter(body)
+		part, err := writer.CreateFormFile("attachment", filename)
+		assert.NoError(t, err)
+		_, err = io.Copy(part, bytes.NewReader(buff.Bytes()))
+		assert.NoError(t, err)
+		err = writer.Close()
+		assert.NoError(t, err)
+
+		req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(body.Bytes())).
+			AddTokenAuth(token).
+			SetHeader("Content-Type", writer.FormDataContentType())
+		resp := MakeRequest(t, req, http.StatusCreated)
+
+		var attachment *api.Attachment
+		DecodeJSON(t, resp, &attachment)
+
+		assert.EqualValues(t, filename, attachment.Name)
+		assert.EqualValues(t, 104, attachment.Size)
+
+		req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=test-asset", bytes.NewReader(body.Bytes())).
+			AddTokenAuth(token).
+			SetHeader("Content-Type", writer.FormDataContentType())
+		resp = MakeRequest(t, req, http.StatusCreated)
+
+		var attachment2 *api.Attachment
+		DecodeJSON(t, resp, &attachment2)
+
+		assert.EqualValues(t, "test-asset", attachment2.Name)
+		assert.EqualValues(t, 104, attachment2.Size)
+	})
+
+	t.Run("application/octet-stream", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(buff.Bytes())).
+			AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusBadRequest)
+
+		req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=stream.bin", bytes.NewReader(buff.Bytes())).
+			AddTokenAuth(token)
+		resp := MakeRequest(t, req, http.StatusCreated)
+
+		var attachment *api.Attachment
+		DecodeJSON(t, resp, &attachment)
+
+		assert.EqualValues(t, "stream.bin", attachment.Name)
+		assert.EqualValues(t, 104, attachment.Size)
+	})
 }