mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:06:51 +08:00
Merge branch 'main' into jzh
This commit is contained in:
commit
bacadc4d35
@ -151,6 +151,12 @@ def deserialize_response(raw_data: bytes) -> Response:
|
||||
|
||||
response = Response(response=body, status=status_code)
|
||||
|
||||
# Replace Flask's default headers (e.g. Content-Type, Content-Length) with the
|
||||
# parsed ones so we faithfully reproduce the original response. Use Headers.add
|
||||
# rather than dict-style assignment so that repeated headers such as Set-Cookie
|
||||
# (and any other multi-valued header per RFC 9110) are preserved instead of
|
||||
# being overwritten.
|
||||
response.headers.clear()
|
||||
for line in lines[1:]:
|
||||
if not line:
|
||||
continue
|
||||
@ -158,6 +164,6 @@ def deserialize_response(raw_data: bytes) -> Response:
|
||||
if ":" not in line_str:
|
||||
continue
|
||||
name, value = line_str.split(":", 1)
|
||||
response.headers[name] = value.strip()
|
||||
response.headers.add(name, value.strip())
|
||||
|
||||
return response
|
||||
|
||||
@ -323,6 +323,50 @@ class TestDeserializeResponse:
|
||||
with pytest.raises(ValueError, match="Invalid status line"):
|
||||
deserialize_response(raw_data)
|
||||
|
||||
def test_deserialize_response_preserves_duplicate_set_cookie_headers(self):
|
||||
# Regression test for https://github.com/langgenius/dify/issues/35722
|
||||
# Multiple Set-Cookie headers must be preserved per RFC 9110, not collapsed
|
||||
# into a single value by dict-style assignment.
|
||||
raw_data = (
|
||||
b"HTTP/1.1 200 OK\r\n"
|
||||
b"Content-Type: text/plain\r\n"
|
||||
b"Set-Cookie: session=abc; Path=/; HttpOnly\r\n"
|
||||
b"Set-Cookie: tracking=xyz; Path=/; Secure\r\n"
|
||||
b"\r\n"
|
||||
b"ok"
|
||||
)
|
||||
|
||||
response = deserialize_response(raw_data)
|
||||
|
||||
cookies = response.headers.getlist("Set-Cookie")
|
||||
assert cookies == [
|
||||
"session=abc; Path=/; HttpOnly",
|
||||
"tracking=xyz; Path=/; Secure",
|
||||
]
|
||||
# Single-valued headers should still be readable normally.
|
||||
assert response.headers.get("Content-Type") == "text/plain"
|
||||
|
||||
def test_deserialize_response_preserves_duplicate_generic_headers(self):
|
||||
# Any header name (not just Set-Cookie) may legitimately repeat; verify the
|
||||
# parser preserves all values rather than overwriting earlier ones.
|
||||
raw_data = b"HTTP/1.1 200 OK\r\nX-Custom: first\r\nX-Custom: second\r\n\r\n"
|
||||
|
||||
response = deserialize_response(raw_data)
|
||||
|
||||
assert response.headers.getlist("X-Custom") == ["first", "second"]
|
||||
|
||||
def test_deserialize_response_does_not_inject_default_content_type(self):
|
||||
# Flask's Response constructor adds a default Content-Type header. When the
|
||||
# raw response has no Content-Type, the parsed response should not silently
|
||||
# gain one from the framework default.
|
||||
raw_data = b"HTTP/1.1 204 No Content\r\nX-Trace-Id: abc\r\n\r\n"
|
||||
|
||||
response = deserialize_response(raw_data)
|
||||
|
||||
header_names = [name for name, _ in response.headers.items()]
|
||||
assert "Content-Type" not in header_names
|
||||
assert response.headers.get("X-Trace-Id") == "abc"
|
||||
|
||||
def test_roundtrip_response(self):
|
||||
# Test that serialize -> deserialize produces equivalent response
|
||||
original_response = Response(
|
||||
|
||||
@ -1744,11 +1744,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/prompt-editor/plugins/component-picker-block/menu.tsx": {
|
||||
"erasable-syntax-only/parameter-properties": {
|
||||
"count": 1
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
} from 'lexical'
|
||||
import * as React from 'react'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import { VAR_REFERENCE_CHILD_POPUP_CLASS_NAME } from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||
@ -928,5 +929,46 @@ describe('ComponentPicker (component-picker-block/index.tsx)', () => {
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('does not hide the menu when focus moves into a variable child popup', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
|
||||
render((
|
||||
<MinimalEditor
|
||||
triggerString="/"
|
||||
workflowVariableBlock={makeWorkflowVariableBlock({}, [
|
||||
makeWorkflowVarNode('node-1', 'Node 1', [
|
||||
makeWorkflowNodeVar('payload', VarType.object, [makeWorkflowNodeVar('child', VarType.string)]),
|
||||
]),
|
||||
])}
|
||||
captures={captures}
|
||||
/>
|
||||
))
|
||||
|
||||
const editor = await waitForEditor(captures)
|
||||
await setEditorText(editor, '/', true)
|
||||
expect(await screen.findByText('payload')).toBeInTheDocument()
|
||||
|
||||
vi.useFakeTimers()
|
||||
|
||||
const popupTarget = document.createElement('button')
|
||||
const popup = document.createElement('div')
|
||||
popup.classList.add(VAR_REFERENCE_CHILD_POPUP_CLASS_NAME)
|
||||
popup.appendChild(popupTarget)
|
||||
document.body.appendChild(popup)
|
||||
|
||||
act(() => {
|
||||
editor.dispatchCommand(BLUR_COMMAND, new FocusEvent('blur-sm', { relatedTarget: popupTarget }))
|
||||
})
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(200)
|
||||
})
|
||||
|
||||
expect(screen.queryByText('payload')).toBeInTheDocument()
|
||||
|
||||
popup.remove()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -14,6 +14,7 @@ import type {
|
||||
WorkflowVariableBlockType,
|
||||
} from '../../types'
|
||||
import type { PickerBlockMenuOption } from './menu'
|
||||
import type { EventEmitterValue } from '@/context/event-emitter'
|
||||
import {
|
||||
flip,
|
||||
offset,
|
||||
@ -39,7 +40,7 @@ import {
|
||||
} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import VarReferenceVars, { VAR_REFERENCE_CHILD_POPUP_CLASS_NAME } from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useBasicTypeaheadTriggerMatch } from '../../hooks'
|
||||
import { $splitNodeContainingQuery } from '../../utils'
|
||||
@ -119,7 +120,9 @@ const ComponentPicker = ({
|
||||
(event) => {
|
||||
clearBlurTimer()
|
||||
const target = event?.relatedTarget as HTMLElement
|
||||
if (!target?.classList?.contains('var-search-input'))
|
||||
const isVariableMenuTarget = target?.classList?.contains('var-search-input')
|
||||
|| target?.closest?.(`.${VAR_REFERENCE_CHILD_POPUP_CLASS_NAME}`)
|
||||
if (!isVariableMenuTarget)
|
||||
blurTimerRef.current = setTimeout(() => setBlurHidden(true), 200)
|
||||
return false
|
||||
},
|
||||
@ -143,8 +146,8 @@ const ComponentPicker = ({
|
||||
}
|
||||
}, [editor, clearBlurTimer])
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
|
||||
eventEmitter?.useSubscription((v: EventEmitterValue) => {
|
||||
if (typeof v !== 'string' && v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND && typeof v.payload === 'string')
|
||||
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`)
|
||||
})
|
||||
|
||||
@ -303,7 +306,7 @@ const ComponentPicker = ({
|
||||
}
|
||||
</>
|
||||
)
|
||||
}, [blurHidden, allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField])
|
||||
}, [blurHidden, allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, triggerString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
|
||||
@ -43,7 +43,7 @@ const Field: FC<Props> = ({
|
||||
disabled={depth !== MAX_DEPTH + 1}
|
||||
render={(
|
||||
<div
|
||||
className={cn('flex items-center justify-between rounded-md pr-2', !readonly && 'hover:bg-state-base-hover', depth !== MAX_DEPTH + 1 && 'cursor-pointer')}
|
||||
className={cn('flex items-center justify-between rounded-md pr-2 outline-none focus:outline-none focus-visible:outline-none', !readonly && 'hover:bg-state-base-hover', depth !== MAX_DEPTH + 1 && 'cursor-pointer')}
|
||||
onMouseDown={() => !readonly && onSelect?.([...valueSelector, name])}
|
||||
>
|
||||
<div className="flex grow items-stretch">
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
} from './var-reference-vars.helpers'
|
||||
|
||||
const VAR_SEARCH_INPUT_CLASS_NAME = 'var-search-input'
|
||||
export const VAR_REFERENCE_CHILD_POPUP_CLASS_NAME = 'var-reference-vars-child-popup'
|
||||
|
||||
const resolveValueSelector = ({
|
||||
itemData,
|
||||
@ -210,7 +211,7 @@ const Item: FC<ItemProps> = ({
|
||||
className={cn(
|
||||
(isObj || isStructureOutput) ? 'pr-1' : 'pr-[18px]',
|
||||
(isHovering || isSelected) && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'),
|
||||
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3',
|
||||
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3 outline-none focus:outline-none focus-visible:outline-none',
|
||||
className,
|
||||
)}
|
||||
data-selected={isSelected ? 'true' : 'false'}
|
||||
@ -263,7 +264,7 @@ const Item: FC<ItemProps> = ({
|
||||
<PopoverContent
|
||||
placement="left-start"
|
||||
sideOffset={0}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
popupClassName={cn(VAR_REFERENCE_CHILD_POPUP_CLASS_NAME, 'border-none bg-transparent p-0 shadow-none backdrop-blur-none')}
|
||||
positionerProps={{
|
||||
style: {
|
||||
zIndex: zIndex || 100,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user