【问题标题】:Jira POST & PUT Rest Calls return Error 400 from PythonJira POST & PUT Rest 调用从 Python 返回错误 400
【发布时间】:2018-04-24 13:36:06
【问题描述】:

我在使用 Jira Rest API 时遇到了一个非常奇怪的问题 - 无论我尝试使用 POST 请求创建问题还是使用对 jira/rest/api/latest/issue/ 的 PUT 请求更新问题时,我都会收到错误代码400 来自 Python 2.7 请求,但它成功来自 Powershell 的 Invoke Web 请求。

但是我可以使用 GET 请求从 Jira 服务器中提取信息,包括:

  • 项目列表
  • 问题类型列表
  • 自定义字段列表
  • 给定项目的给定类型的问题
  • Zephyr 插件的测试执行
  • Zephyr 插件的测试周期

我已经尝试了来自 Atlassian 支持网站上类似主题的一些故障排除建议:

  • 我已经验证授权是正确的(它也是所有工作的 GET 请求所必需的)
  • 我正在使用对 Jira 实例具有管理员级别访问权限的帐户进行测试
  • 我已将 json 剥离为仅与此处的 REST API 文档匹配的字段和格式:jira-rest-api-examples/#creating-an-issue-examples
  • 我已确保所有相关的会话 cookie 和标头数据都已存储并添加到后续请求中
  • 我已通过 Issue/createmeta 验证我有能力创建该问题类型(正如我之前指出的 - 它适用于 Powershell)
  • 我尝试使用 Issuetype 名称和 id 以及项目密钥和 id 作为标识符,都没有改变任何东西
  • 我什至尝试在 /issue 路径上省略和包含尾部斜杠,以防万一这很重要
  • 我已经验证这不是 Python 的用户代理因 POST/PUT 请求而被阻止的情况

Json 主体(原始):

{"fields": {"issuetype": {"id": "10702"}, "project": {"id": "10061"}, "description": "Execution for Issue: SDBX-859", "summary": "Execution for Issue: SDBX-859"}}

(为便于阅读而格式化):

{
    "fields": {
        "issuetype": {
            "id": "10702"
        },
        "project": {
            "id": "10061"
        },
        "description": "Execution for Issue: SDBX-859",
        "summary": "Execution for Issue: SDBX-859"
    }
}

流程从这个类开始:

class Migrator(object):
    RestURLs = {
        "projects": "api/latest/project",
        "issuetype": "api/latest/issuetype",
        "fields": "api/latest/field",
        "tests": "api/latest/search?maxResults={limit}&expand=meta&jql=IssueType='{testType}'+and+project={projectKey}",
        "zSteps": "zapi/latest/teststep/{issueId}",
        "zExecutions": "zapi/latest/zql/executeSearch?zqlQuery=project={projectKey}",
        "zCycles": "zapi/latest/cycle?projectId={projectId}",
        "issue": "api/latest/issue/{issueKey}",
        "xSteps": "raven/1.0/api/test/{issueKey}/step/{stepId}",
        "xSet": "raven/1.0/api/testset/{issueKey}/test",
        "xExecution": "raven/1.0/api/testexec/{issueKey}/test"
    }

    CustomFields = {
        "Zephyr Teststep": "",
        "Manual Test Steps": "",
        "Test Type": ""
    }

    IssueNames = {
        "zephyr":"Zephyr - Test",
        "xray":"Test",
        "set":"Test Set",
        "execution":"Test Execution"
    }
    IssueTypes = {}

def __init__(self):
    self.results = []
    print("new Migrator initialised")
    self.restHandler = RestHandler()
    self.baseURL = "http://127.0.0.1/jira/rest/"
    self.authentication = ""
    self.commonHeaders = {}
    self.projectList = []
    self.project = None
    self.testList = []
    self.executionList = {}
    self.versionList = set()
    self.cycleList = {}
    self.setList = []

def connect(self, username, password, serverUrl="http://127.0.0.1"):
    # 1 - connect to jira
    if serverUrl[-1] != '/':
        serverUrl += '/'
    self.baseURL = str.format("{0}jira/rest/", serverUrl)
    self.authentication = "Basic " + base64.b64encode(username + ":" + password)
    self.commonHeaders = {"Authorization": self.authentication}

    print("Connecting to Server: " + self.baseURL)
    headers = self.commonHeaders
    projList = self.restHandler.perform(method=HTTP.GET,url=self.baseURL,path=Migrator.RestURLs["projects"],headers=headers)

    # 2 - populate projects list
    for projDict in projList:
        self.projectList.append(Project().fromDict(projDict))

从这个方法:

    def migrateExecutions(self, project):
        print "working..."
        for execution in self.executionList:
            # Restricting it only to my issues for testing...
            if execution.assigneeUserName == "boydnic":
                headers = self.commonHeaders
                execData = {"fields":{}}
                execData["fields"]["issuetype"] = {"id":self.IssueTypes[self.IssueNames["execution"]].id}
                execData["fields"]["project"] = {"id":project.id}
#                execData["fields"]["reporter"] = {"name": userName}
#                execData["fields"]["assignee"] = {"name": execution.assigneeUserName}
                execData["fields"]["summary"] = "Execution for Issue: " + execution.issueKey
                execData["fields"]["description"] = execution.comment if execution.comment else execData["fields"]["summary"]

                xrayExec = self.createIssue(execData)
                self.results.append(self.restHandler.perform(method=HTTP.POST, url=self.baseURL,
                                         path=self.RestURLs["xExecution"], urlData={"issueKey":xrayExec.key},
                                         headers=headers, body={"add":[execution.issueKey]}))

到这个方法:

def createIssue(self, issueTemplate):
    result = self.restHandler.perform(method=HTTP.POST, url=self.baseURL, path=Migrator.RestURLs["issue"], urlData={"issueKey":""}, headers=self.commonHeaders, body=issueTemplate)
    issue = Issue()
    issue.id = result["id"]
    issue.key = result["key"]
    issue.self = result["self"]
    print("Created Issue: "+issue.key)
    return issue

它自己调用这个类:

class RestHandler(object):
    def __init__(self):
        self.headerStore = {'X-CITNET-USER':"",
                            'X-ASEN':"",
                            'X-ASESSIONID':"",
                            'X-AUSERNAME':""}
        self.cookieJar = requests.cookies.RequestsCookieJar()

    def perform(self, method, url, path, headers={}, urlData={"projectId": "", "projectKey": "", "issueId": "", "issueKey": ""},
                formData=dict(), body=""):
        resultData = "{}"
        path = url + path.format(**urlData)
        body = body if isinstance(body, str) else json.dumps(body)
        if self.headerStore:
            headers.update(self.headerStore)
        jar = self.cookieJar
        print(str(method))
        print(path)
        if method is HTTP.GET:
            resultData = requests.get(path, headers=headers, cookies = jar)
        elif method is HTTP.POST:
            print (body)
            path = path.rstrip('/')
            resultData = requests.post(path, json=body, headers=headers, cookies = jar)
        elif method is HTTP.PUT:
            print (body)
            resultData = requests.put(path, json=body, headers=headers, cookies = jar)
        elif method is HTTP.DELETE:
            request = "DELETE request to " + path
        else:
            raise TypeError
        print("\n\n===============================\nRest Call Debugging\n===============================")
        print(resultData)
        print(resultData.url)
        print(resultData.status_code)
        print(resultData.headers)
        print(resultData.content)
        print("\n\n===============================\n/Rest Call Debugging\n==============================")
        if 199 < resultData.status_code < 300:
            for hKey, hValue in resultData.headers.iteritems():
                if hKey in self.headerStore.keys():
                    self.headerStore[hKey] = hValue
            self.cookieJar.update(resultData.cookies)
            print "testing breakpoint"
            return json.loads(resultData.content)
        else:
            raise IOError(resultData.reason)

Rest Handler 类中包含的调试部分只是吐出以下内容:

===============================
Rest Call Debugging
===============================
https://webgate.test.ec.europa.eu/CITnet/jira/rest/api/latest/issue
400
{'X-AUSERNAME': 'boydnic', 'X-AREQUESTID': '<redacted>', 'X-Content-Type-Options': 'nosniff', 'Transfer-Encoding': 'chunked', 'Set-Cookie': 'crowd.token_key=""; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Path=/; HttpOnly, crowd.token_key=<redacted>; Path=/; HttpOnly, JSESSIONID=<redacted>; Path=/CITnet/jira; HttpOnly, atlassian.xsrf.token=<redacted>; Path=/CITnet/jira', 'X-Seraph-LoginReason': 'OUT, OK', 'X-ASEN': '<redacted>', 'X-CITNET-USER': 'boydnic', 'Connection': 'Keep-Alive', 'X-ASESSIONID': '<redacted>', 'Cache-Control': 'no-cache, no-store, no-transform, proxy-revalidate', 'Date': 'Tue, 24 Apr 2018 08:29:16 GMT', 'Server': 'Apache-Coyote/1.1', 'Content-Type': 'application/json;charset=UTF-8'}
{"errorMessages":["Can not instantiate value of type [simple type, class com.atlassian.jira.rest.v2.issue.IssueUpdateBean] from JSON String; no single-String constructor/factory method"]}


===============================
/Rest Call Debugging
==============================

与此 I/O 错误混杂在一起:

(我为这篇文章解开了 STD 和 ERR 流)

Traceback (most recent call last):   File
"C:/Users/BOYDnic/Documents/migrator/issueMigrator.py", line 546, in
<module> <Response [400]>
    jiraMigrator.migrate(projectKey)
      File "C:/Users/BOYDnic/Documents/migrator/issueMigrator.py", line 330, in migrate
    self.migrateExecutions(project)   File "C:/Users/BOYDnic/Documents/migrator/issueMigrator.py", line 475, in
migrateExecutions
    xrayExec = self.createIssue(execData)   File "C:/Users/BOYDnic/Documents/migrator/issueMigrator.py", line 334, in
createIssue
    result = self.restHandler.perform(method=HTTP.POST, url=self.baseURL, path=Migrator.RestURLs["issue"],
urlData={"issueKey":""}, headers=self.commonHeaders,
body=issueTemplate)   File
"C:/Users/BOYDnic/Documents/migrator/issueMigrator.py", line 84, in
perform
    raise IOError(resultData.reason) IOError: Bad Request

我对此感到非常沮丧,尤其是因为它阻止了此迁移脚本的完成并且似乎没有任何意义。

【问题讨论】:

  • 你能通过失败的原始http请求吗?

标签: python rest python-requests jira jira-rest-api


【解决方案1】:

当请求正文的格式不正确时,Jira 会返回诸如“无法实例化类型的值 ...”之类的错误。在您的情况下,您提供了一个字符串,其中 Jira 需要更复杂的内容(通常是 dict)。

【讨论】:

  • 为响应干杯,POST 请求的正文最初更复杂(包括 Reporter 和 Assignee),但我将其修剪以匹配 Jira Rest 文档中给出的基本示例正文的结构(上面链接)。
【解决方案2】:

原来是我将json.dumps(body)put(..., json=body, ...) 结合使用导致了问题。

使用 json 关键字告诉 Requests 再次序列化字符串,将 " 标记转义为 \" 并再次用引号括起来。

有效地:

{"fields": {"issuetype": {"id": 10702},"project": {"id":10061},"description": "","summary": "Execution for Issue: SDBX-859"}}

成为:

"{\"fields\": {\"issuetype\": {\"id\": \"10702\"}, \"project\": {\"id\": \"10061\"}, \"description\": \"Execution for Issue: SDBX-859\", \"summary\": \"Execution for Issue: SDBX-859\"}}"

body=json.dumps({...}) 与手动设置的内容标题一起使用或json={...} 不能同时使用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-08-03
    • 2012-01-20
    • 2015-07-05
    • 1970-01-01
    • 1970-01-01
    • 2010-12-10
    • 2019-07-28
    • 1970-01-01
    相关资源
    最近更新 更多